MVC actionlink passing list of objects - c#

I've got a view that looks like this:
With this view I'm filtering records which are in a database. Each "filter" is a SearchViewModel which has a class definition like this:
public class SearchViewModel
{
//Property has a property called "SqlColumnName"
public Property Property { get; set; }
public Enums.SearchOperator Operator { get; set; }
public string Value { get; set; }
}
I'm now trying to find a solution how to build a actionlink to this site and passing in a List<SearchViewModel>().
So I'm trying to accomplish something like this:
http://url/Index?property=Typ&Operator=2&Value=4&property=ServerName&Operator=1&Value=server
I've tried to solve my problem with this http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/ but this guy is calling the Action with a POST and i need to call it with a GET request.
So how to pass a list of objects to a mvc controller action with an actionlink?
EDIT
I think i need to say, that POST request is not an option because i need that link in an onClick event of a div element.

You could construct a link with the following parameters that should work:
http://url/Index?%5B0%5D0.property=Typ&%5B0%5D0.Operator=2&%5B0%5D0.Value=4&%5B1%5D0.property=ServerName&%5B1%5D0.Operator=1&%5B1%5D0.Value=server
Also notice that there are restrictions in the length of query string parameters. Those restrictions vary between the browsers. So it's probably a better approach to simply generate a link with an id that would allow you to retrieve the corresponding collection on the server (from your datastore):
http://url/Index?id=123

ActionLink helper method renders an anchor tag. It is ok to pass few query string items via a link. Remember Query string has a limit about how much data you can pass and it varies from browser to browser.
What you should be doing is a form posting. You can do a form posting on a click event on a div with the help of a little javascript.
Let's create a new view model for our search page
public class SearchVm
{
public List<SelectListItem> Operators { set; get; }
public List<SearchViewModel> Filters { set; get; }
}
public class SearchViewModel
{
//Property has a property called "SqlColumnName"
public Property Property { get; set; }
public SearchOperator Operator { get; set; }
public string Value { get; set; }
}
So in your GET action, You will send a list of SearchViewModel to the view.
public ActionResult Index()
{
var search = new SearchVm
{
Filters = new List<SearchViewModel>
{
new SearchViewModel {Property = new Property {SqlColumn = "Name"}},
new SearchViewModel {Property = new Property {SqlColumn = "Age"}},
new SearchViewModel {Property = new Property {SqlColumn = "Location"}}
}
};
//Convert the Enums to a List of SelectListItem
search.Operators= Enum.GetValues(typeof(SearchOperator)).Cast<SearchOperator>()
.Select(v => new SelectListItem
{
Text = v.ToString(),
Value = ((int)v).ToString()
}).ToList();
return View(search);
}
And in your view, which is strongly typed to your SearchVm view model, We will manipulate the form field names so that the model binding will work when the form is submitted.
#model SearchVm
#using (Html.BeginForm())
{
var i = 0;
foreach (var criteria in Model.Filters)
{
<label>#criteria.Property.SqlColumn</label>
#Html.HiddenFor(f => f.Filters[i].Property.SqlColumn)
#Html.DropDownList("Filters[" + i+ "].Operator",Model.Operators)
#Html.TextBoxFor(f=>f.Filters[i].Value)
i++;
}
<div id="someDiv">Search button using Div</div>
<input type="submit" value="Search" />
}
And your HttpPost action method to handle the form submit.
[HttpPost]
public ActionResult Index(SearchVm model)
{
foreach(var f in model.Filters)
{
//check f.Property.SqlColumn, f.Value & f.Operator
}
// to do :Return something useful
}
If you want the form to be submitted on the click event on the div, Listen to the click event on the specific div and call the submit method on the form the div resides in.
<script>
$(function () {
$("#someDiv").click(function(e) {
$(this).closest("form").submit();
});
});
</script>

Related

Razor bind radiobox to list of SelectListItem

I have a basic model
public class SpeakerConsent
{
public string FieldLabel { get; set; }
public List<SelectListItem> OptionValues { get; set; }
}
My razor page currently looks like the following
#for (var i = 0; i < Model.OptionValues.Count(); i++)
{
#Html.RadioButtonFor(x => Model.OptionValues[i].Selected, Model.OptionValues[i].Value )
}
I might have 4 or 5 items in my OptionValues.
Question : How can i bind to the Selected property of the SelectListItem so when the model is posted i can identify which on of the radio buttons has been selected?
With your current code, the HTML markup generated for the input element will be like below
<input data-val="true" name="OptionValues[0].Selected" type="radio" value="201">
the name is Selected and the value is a number. The Selected property of the SelectListItem is bool type. Model binding will not work as expected when you post this form data.
IMHO, The easiest way to handle a scenario like this is to use editor templates.
I will first create a class which represents the data I want to use with the radio buttons.
public class MyOption
{
public bool IsSelected { set; get; }
public int Id { set; get; }
public string Text { set; get; }
}
Now in my main view model, I will add a collection type property
public class SpeakerConsent
{
public List<MyOption> Options { set; get; }
}
Now in your ~/Views/Shared/ directory, create a folder called EditorTemplates and then create a view called MyOption.cshtml in that. You can have the HTML markup you want to use to render the radio button in that. Here I am keeping the Id property in a hidden field along with the radio button and label.
#model YourNamespace.MyOption
<div>
<span>#Model.Text</span>
#Html.HiddenFor(g=>g.Id)
#Html.RadioButtonFor(b => b.IsSelected, true)
</div>
Now in your GET action, you can populate this Options property of yout SpeakerConsent view model object and send to the view.
public ActionResult Create()
{
var vm = new SpeakerConsent();
vm.Options = new List<MyOption>
{
new MyOption { Id=1, Text="Seattle"},
new MyOption { Id=2, Text="Detroit"},
new MyOption { Id=31, Text="Kerala"},
};
return View(vm);
}
Now in your main view, you can simply call the EditorFor helper method.
#model YourNamespace.SpeakerConsent
#using (Html.BeginForm("Index", "Home"))
{
#Html.EditorFor(a=>a.Options)
<button type="submit">Save</button>
}
Make sure to check the HTML markup generated by this code. notice the
name attribute value of the inputs.
When the form is submitted, you can read the Options property value, loop through them and read the Id and IsSelected property value and use them as needed
[HttpPost]
public ActionResult Create(SpeakerConsent model)
{
// check model.Options
// to do : return something
}

ASP.NET Core MVC Model Binding Bug?

I'm having some trouble with ASP.NET Core's model binding. Basically I'm just trying to bind some POSTed json to an object model with nested properties. Minimal code is provided below for a single button that, when pressed, will POST to a Controller action method via XMLHttpRequest. The action method takes a single model class parameter with the [FromBody] attribute.
Model:
public class OuterModel {
public string OuterString { get; set; }
public int OuterInt { get; set; }
public InnerModel Inner { get; set; }
}
public class InnerModel {
public string InnerString { get; set; }
public int InnerInt { get; set; }
}
Controller:
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller {
[HttpPost("models/")]
public IActionResult Save([FromBody] OuterModel viewModel) {
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Return an appropriate response
return Ok();
}
}
Razor markup for a "submit" button:
<div class="form-row">
<div class="col-2">
#{ string url = Url.Action(nameof(HomeController.Save), "Home"); }
<button id="post-btn" data-post-url="#url">POST</button>
</div>
</div>
JavaScript to submit (DOES NOT bind):
document.getElementById("post-btn").addEventListener("click", e => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("timeout", () => console.error("Timeout"));
xhr.addEventListener("error", () => console.error("Error"));
xhr.addEventListener("load", () => console.log(`Status: ${xhr.status} ${xhr.statusText}`));
xhr.open("POST", e.target.dataset.postUrl);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(JSON.stringify({
"OuterString": "outer",
"OuterInt": 1,
"Inner.InnerString": "inner",
"Inner.InnerInt": 5
}));
});
Looking at that JavaScript, I would expect the Inner.* json property names to bind correctly, given this line from the docs:
Model binding looks for the pattern parameter_name.property_name to bind values to properties. If it doesn't find matching values of this form, it will attempt to bind using just the property name.
But it doesn't; the OuterModel.Inner property in the action method ends up null. The following json does bind correctly, however:
JavaScript to submit (DOES bind):
xhr.send(JSON.stringify({
"OuterString": "outer",
"OuterInt": 1,
"Inner": {
"InnerString": "inner",
"InnerInt": 5
}
}));
So I can use this code to achieve the model binding that I need, I'm just confused as to why the first JavaScript didn't work. Was I not using the correct naming convention for nested properties? Some clarification would be much appreciated!

MVC Pass display data between views

I have a view model that is used to display a form on one view, and then is also used to represent the POST data to an action. The action then displays another view model that contains much of the same data from the first view model. However, the first view model has several "display only" properties that are also required on the second view model (for display only on the second view also).
I am wondering what the best way to pass this "display only" data to the second view would be. Currently, the best solution I have come up with is to have a bunch of hidden form fields that contain the display only property values, and then the model gets auto-populated for the action that handles the form POST. However, using hidden form fields seems very "hackish", and there seems like there should be a better solution to passing this data to another view The action doesn't need the display only information, it is only accessing it to populate the properties of the second view model that is passed to the second view.
Let me just explain my question with code, as what I am after is probably better understood through code than words.
Models:
public class SearchFilters
{
// ...
}
public class SearchResult
{
public int Id { get; set; }
public bool Selected { get; set; }
public string SomeDisplayValue1 { get; set; }
public string SomeDisplayValue2 { get; set; }
// ...
}
public class ResultsViewModel
{
public IList<SearchResult> Results { get; set; }
// ...
}
public class DoSomethingWithSelectedResultsViewModel
{
public IList<SearchResult> SelectedResults { get; set; }
public string SomeOtherProperty { get; set; }
// ...
}
Controller:
[HttpPost]
public ActionResult Results(SearchFilters filters)
{
ResultsViewModel results = new ResultsViewModel();
// ...
return new View(results);
}
[HttpPost]
public ActionResult DoSomethingWithSelectedResults(ResultsViewModel model)
{
// ...
return View(new DoSomethingWithSelectedResultsViewModel
{
SelectedResults = model.Results.Where(r => r.Selected).ToList(),
SomeOtherProperty = "...",
// ...
});
}
View: Results.cshtml
#model ResultsViewModel
#using (Html.BeginForm("DoSomethingWithSelectedResults", "Search"))
{
<table>
for (int i = 0; i < Model.Results.Count; i++)
{
<tr>
<td>
#Html.CheckBoxFor(m => Model.Results[i].Selected)
#* I would like to eliminate these hidden inputs *#
#Html.HiddenFor(m => Model.Results[i].Id)
#Html.HiddenFor(m => Model.Results[i].SomeDisplayValue1)
#Html.HiddenFor(m => Model.Results[i].SomeDisplayValue2)
</td>
<td>#Html.DisplayFor(m => Model.Results[i].SomeDisplayValue1)</td>
<td>#Html.DisplayFor(m => Model.Results[i].SomeDisplayValue2)</td>
<tr>
}
</table>
<button type="submit">Do Something With Selected Results</button>
}
As far as I know, one of the best way to pass data from View to another View through a Controller is to use ViewBag, ViewData or TempData. As an example, you can pass the data retrieved from View I as shown below:
TempData[DataToBePassed] = model.CustomData;
And then retrieve this data in View II similar to that:
#if(TempData[DataToBePassed] != null)
{
var dataFromFirstView = TempData[DataToBePassed];
}
For more information take a look at When to use ViewBag, ViewData, or TempData in ASP.NET MVC 3 applications.
You could put the model in the TempData property of the controller, that way it's automatically available in the next request.
More here
Found what I was looking for, I just hadn't worked with MVC enough yet to know about it. The Controller.UpdateModel method does exactly what I was looking for.
Example (using the code from the question):
[HttpPost]
public ActionResult DoSomethingWithSelectedResults()
{
// Load initial model data here, in this case I had simply cached the results in
// temp data in the previous action as suggested by Emeka Awagu.
ResultsViewModel model = (ResultsViewModel)TempData["results"];
// Call UpdateModel and let it do it's magic.
UpdateModel(model);
// ...
return View(new DoSomethingWithSelectedResultsViewModel
{
SelectedResults = model.Results.Where(r => r.Selected).ToList(),
SomeOtherProperty = "...",
// ...
});
}
Using this method I was able to eliminate all the hidden form fields and did not have to write any custom copy logic, since UpdateModel deals with it automatically.
Note: I did have to implement some custom model binders to get things to work correctly with dictionaries and collections (see here, here, and here).

Model dropping List<>s after DropDownList

I'm working on a search criteria building page. In addition to several string and numerical type fields, there are several "multiple choice" options.
I'm using the [Get] signature without parameters(pass the CriteriaModel to the view) >> [Post] signature with CriteriaModel parameter (redirect to searching controller)
I've built lightweight option classes (just value, name pairs) and am populating several List<> with the primitive options.
Using Html.DropDownListFor, I'm able to get them to display.
...but...
When I enter the [Post] version, the List<>s are all set to null and empty. Further, the other criteria fields supposed to be populated afterwards are also default and empty.
Technically, I don't need a whole list of values back - if I could even just have the index of the selected value - but I'm up against a wall here.
Pertinent model data:
public class CriteriaModel
{
[DisplayName("Owner Name")]
public string OwnerName { get; set; }
[DisplayName("Subdivision")]
public List<Subdivision> Subdivision { get; set; }
[DisplayName("PIN")]
public string PIN { get; set; }
}
public class Subdivision
{
public int ID { get; set; }
public string Name { get; set; }
}
Pertinent controller code:
[HttpGet]
public ActionResult Index()
{
CriteriaModel criteria = new CriteriaModel();
...fill in the Subdivisions...
View(criteria);
}
[HttpPost]
public ActionResult Index(CriteriaModel search_criteria)
{
return View("Search obtained" + search_criteria.Subdivision.First().Name);
}
And pertinent View markup:
#model REOModern.Models.CriteriaModel
...bunch of HTML...
#Html.LabelFor(model => model.Subdivision)
#Html.DropDownListFor(x => x.Subdivision, new SelectList(Model.Subdivision, "ID", "Name", Model.Subdivision.First().ID))
...other HTML...
<button type="submit" class="btn btn-primary" value="Index">Search</button>
I should clarify: I know that my 'return View("Search obtained" + ...' will fail, but it should show the piece of data that I need. The problem is it's a null reference exception. Until I can fix that, there's no point in building a user-friendly View for submitted search criteria.
MVC does not repopulate the List<> elements.
You would split the selected value out into another property of the model.
So in your model, include something like this
public int SelectedValue { get; set; }
Then for your Html.DropDownListFor helper you would use
Html.DropDownListFor(model => model.SelectedValue, Model.DropDownList, new { /* htmlAttributes */ });
Of course they're empty. The only data that exists in your post action is that which was posted via the form. Since the entire dropdown list, itself, was not posted, merely a selected item(s), the lists are empty. For anything like this, you need to rerun the same logic in your post action to populate them as you did in your get action. It's usually better to factor out this logic into a private method on your controller that both actions can use:
private void PopulateSomeDropDownList(SomeModel model)
{
// logic here to construct dropdown list
model.SomeDropDownList = dropdownlist;
}
Then in your actions:
PopulateSomeDropDownList(model);
return View(model);

Can I create drop down list while using editor for?

How do I create drop down list using data annotation?
I would like to achieve markup generated by
#Html.DropDownListFor(x=>x.ContactType, Model.ContactTypeOptions)
to be set so I can use and it would generate dropdown list:
#Html.EditorForModel(Model)
My current model is:
public class ContactModel
{
public string ContactType { get; set; }
public IList<SelectListItem> ContactTypeOptions
{
get
{
return new List<SelectListItem>()
{
new SelectListItem(){Text = "Options"}
};
}
}
[Required(AllowEmptyStrings = false)]
[MinLength(15)]
[DataType(DataType.MultilineText)]
public string Message { get; set; }
}
Updated
I do not want to use partial view.
You could try something like this:
public class ContactModel
{
[UIHint("_DropDownList")]
public SelectList ContactType { get; set; }
}
Set (in your controller) ContactType.Items to be your list of options and ContactType.SelectedValue to be your initially selected value.
Then define a partial view _DropDownList.cshtml:
#model SelectList
#Html.DropDownListFor(m => m.SelectedValue, Model)
You should then be able to use #Html.EditorFor(m => m.ContactType) and get your dropdown. And you can reuse it anywhere!
You might even get this behaviour out of the box nowadays when you do #Html.EditorFor(m => m.Property) where m.Property is a SelectList. Not sure on that one.
If you're looking to just use EditorForModel() on your ContactModel, then you can just create an editor template called ContactModel.cshtml and do:
#model ContactModel
#Html.DropDownListFor(m => m.ContactType,
new SelectList(Model.ContactTypeOptions, Model.ContactType))
Note that this should be called as #Html.EditorForModel() in a view already typed to a ContactModel- the object passed in as a parameter in the overload EditorForModel(Object) is for additional view data, not for the model object. EditorForModel always renders an editor template for the current view model.

Categories