I have a dynamic list of dynamic lists, which have <input />s that need to be POSTed to an MVC controller/action and bound as a typed object. The crux of my problem is I can't figure out how to manually pick out arbitrary POSTed form values in my custom model binder. Details are below.
I have a list of US States that each have a list of Cities. Both States and Cities can be dynamically added, deleted, and re-ordered. So something like:
public class ConfigureStatesModel
{
public List<State> States { get; set; }
}
public class State
{
public string Name { get; set; }
public List<City> Cities { get; set; }
}
public class City
{
public string Name { get; set; }
public int Population { get; set; }
}
The GET:
public ActionResult Index()
{
var csm = new ConfigureStatesModel(); //... populate model ...
return View("~/Views/ConfigureStates.cshtml", csm);
}
The ConfigureStates.cshtml:
#model Models.ConfigureStatesModel
#foreach (var state in Model.States)
{
<input name="stateName" type="text" value="#state.Name" />
foreach (var city in state.Cities)
{
<input name="cityName" type="text" value="#city.Name" />
<input name="cityPopulation" type="text" value="#city.Population" />
}
}
(There is more markup and javascript, but I leave it out for brevity/simplicity.)
All form inputs are then POSTed to server, as so (parsed by Chrome Dev Tools):
stateName: California
cityName: Sacramento
cityPopulation: 1000000
cityName: San Francisco
cityPopulation: 2000000
stateName: Florida
cityName: Miami
cityPopulation: 3000000
cityName: Orlando
cityPopulation: 4000000
I need to capture the form values, ideally bound as a List<State> (or, equivalently, as a ConfigureStatesModel), as so:
[HttpPost]
public ActionResult Save(List<State> states)
{
//do some stuff
}
A custom model binder seems like the right tool for the job. But I don't know how to know which city names and city populations belong to which state names. That is, I can see all the form keys and values POSTed, but I don't see a way to know their relation:
public class StatesBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//California, Florida
List<string> stateNames = controllerContext.HttpContext.Request.Form.GetValues("stateName").ToList();
//Sacramento, San Francisco, Miami, Orlando
List<string> cityNames = controllerContext.HttpContext.Request.Form.GetValues("cityName").ToList();
//1000000, 2000000, 3000000, 4000000
List<int> cityPopulations = controllerContext.HttpContext.Request.Form.GetValues("cityPopulation")
.Select(p => int.Parse(p)).ToList();
// ... build List<State> ...
}
}
If I could just know the order all values came in in relation to all other form values, that would be enough. The only way I see to do this is looking at the raw request stream, as so:
Request.InputStream.Seek(0, SeekOrigin.Begin);
string urlEncodedFormData = new StreamReader(Request.InputStream).ReadToEnd();
but I don't want to be messing with manually parsing that.
Also note that the order of the list of states and the order of the lists of cities in each state matter, as I persist the concept of display-order for them. So that would need to be preserved from the form values as well.
I've tried variations of dynamic list binding like this and this. But it feels wrong junking up the html and adding a lot of (error-prone) javascript, just to get the binding to work. The form values are already there; it should just be a matter of capturing them on the server.
The only obvious way I see of building a form that will actually represent which cities belong to which state would require that you use the strongly-typed helpers.
So, I'd use something similar to:
#model Models.ConfigureStatesModel
#for (int outer = 0; outer < Model.States.Count; outer++)
{
<div class="states">
#Html.TextBoxFor(m => m.States[outer].Name, new { #class="state" })
for (int inner = 0; inner < Model.States[outer].Cities.Count; inner++)
{
<div class="cities">
#Html.TextBoxFor(m => m.States[outer].Cities[inner].Name)
#Html.TextBoxFor(m => m.States[outer].Cities[inner].Population)
</div>
}
</div>
}
This will create inputs with form names that the default modelbinder can handle.
The part that requires some additional work is handling the re-ordering. I would use something like this, assuming you are using jQuery already:
// Iterate through each state
$('.states').each(function (i, el) {
var state = $(this);
var input = state.find('input.state');
var nameState = input.attr('name');
if (nameState != null) {
input.attr('name', nameState.replace(new RegExp("States\\[.*\\]", 'gi'), '[' + i + ']'));
}
var idState = input.attr('id');
if (idState != null) {
input.attr('id', idState.replace(new RegExp("States_\\d+"), i));
}
// Iterate through the cities associated with each state
state.find('.cities').each(function (index, elem) {
var inputs = $(this).find('input');
inputs.each(function(){
var cityInput = (this);
var nameCity = cityInput.attr('name');
if (nameCity != null) {
cityInput.attr('name', nameCity.replace(new RegExp("Cities\\[.*\\]", 'gi'), '[' + index + ']'));
}
var idCity = cityInput.attr('id');
if (idCity != null) {
cityInput.attr('id', idCity.replace(new RegExp("Cities_\\d+"), index));
}
});
});
});
This last bit probably requires some tweaking, as it's untested, but it's similar to something I've done before. You would call this whenever the items on your view are added/edited/removed/moved.
I came up with my own solution. It's a little bit of a hack, but I feel it's better than the alternatives. The other solution and suggestions all involved altering the markup and adding javascript to synchronize the added markup -- which I specifically said I did not want to do in the OP. I feel adding indexes to the <input /> names is redundant if said <input />s are already ordered in the DOM the way you want them. And adding javascript is just one more thing to maintain, and unnecessary bits sent through the wire.
Anyways .. My solution involves looping through the raw request body. I hadn't realized before that this is basically just a url-encoded querystring, and it's easy to work with after a simple url-decode:
public class StatesBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
controllerContext.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin);
string urlEncodedFormData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
var decodedFormKeyValuePairs = urlEncodedFormData
.Split('&')
.Select(s => s.Split('='))
.Where(kv => kv.Length == 2 && !string.IsNullOrEmpty(kv[0]) && !string.IsNullOrEmpty(kv[1]))
.Select(kv => new { key = HttpUtility.UrlDecode(kv[0]), value = HttpUtility.UrlDecode(kv[1]) });
var states = new List<State>();
foreach (var kv in decodedFormKeyValuePairs)
{
if (kv.key == "stateName")
{
states.Add(new State { Name = kv.value, Cities = new List<City>() });
}
else if (kv.key == "cityName")
{
states.Last().Cities.Add(new City { Name = kv.value });
}
else if (kv.key == "cityPopulation")
{
states.Last().Cities.Last().Population = int.Parse(kv.value);
}
else
{
//key-value form field that can be ignored
}
}
return states;
}
}
This assumes that (1) the html elements are ordered on the DOM correctly, (2) are set in the POST request body in the same order, and (3) are received in the request stream on the server in the same order. To my understanding, and in my case, these are valid assumptions.
Again, this feels like a hack, and doesn't seem very MVC-y. But it works for me. If this happens to help someone else out there, cool.
Related
apologies if I'm doing something wrong, this is my first post.
I'm currently working with C# and want to save a bunch of data out to a JSON file and load it back, but I'm having trouble figuring out how to get it in the following format.
// Primary ID
001
{
// Secondary ID
01
{
// Tertiary ID
01
{
string: "this is some information.",
int: 9371
}
}
// Secondary ID
02
{
// Tertiary ID
01
{
string: "blah blah blah.",
int: 2241
}
}
}
I'd essentially like to be able to call up information with a particular set of IDs for example 001-02-01 which would return a string ("blah blah blah.") and an int (2241).
The reason I want to go about it like this instead of just having one longer ID is so that when the JSON file becomes very large, I'm hoping to be able to speed up the search for information by passing each ID in turn.
If that makes no sense and it would be equally as fast to just pass in one longer ID and not be bothered by this whole nested ID segments concept then please let me know!
If, however what I'm thinking is correct and it would help the speed of finding particular data by structuring it out like this, how would I go about doing that? With nested C# classes in arrays?
The most simple way and efficient way would be to have all data as same type. Currently, you seem to go for each object is of type of the given id:
{
"01":{},
"02" :{}
}
this will not go too well if trying to use a serializable class.
I would recommend the following:
{
"items" : [
{"id":"01" }, { "id":"02" },...
]
}
Then you can serialize/deserialize easily with
[Serializable]
public class Item
{
public string id = null;
}
[Serializable]
public class RootObject
{
public List<Item> items = null;
}
and then in Unity:
void Start(){
string str = GetJson(); // However you get it
RootObject ro = JsonUtility.FromJson<RootObject>(str);
}
if you want to speed up the fetching and your collection is large, convert to dictionary.
Dictionary<string, Item> dict = null;
void Start(){
string str = GetJson(); // However you get it
RootObject ro = JsonUtility.FromJson<RootObject>(str);
this.dict = new Dictionary<string,Item>();
foreach(Item item in ro.items){
Item temp = temp;
this.dict.Add(item.Id, temp);
}
ro = null;
}
Now you can access real fast.
Item GetItem(string id)
{
if(string.IsNullOrEmpty(id) == true){ return null; }
Item item = null;
this.dict.TryGetValue(id, out item);
return item;
}
If you end up storing millions of records in your file and want to start doing something more performant it would be easier to switch to a decent document database like MongoDB rather than trying to reinvent the wheel.
Worry about writing good standard code before worrying about performance problems that don't yet exist.
The following example is not in your language of choice but it does explain that JSON and arrays of 1,000,000 objects can be searched very quickly:
const getIncidentId = () => {
let id = Math.random().toString(36).substr(2, 6).toUpperCase().replace("O", "0")
return `${id.slice(0, 3)}-${id.slice(3)}`
}
console.log("Building array of 1,000,000 objects")
const littleData = Array.from({ length: 1000000 }, (v, k) => k + 1).map(x => ({ cells: { Number: x, Id: getIncidentId() } }))
console.log("Getting list of random Ids for array members [49, 60, 70000, 700000, 999999]")
const randomIds = ([49, 60, 70000, 700000, 999999]).map(i => littleData[i].cells.Id)
console.log(randomIds)
console.log("Finding each array item that contains a nested Id property in the randomIds list.")
const foundItems = littleData.filter(i => randomIds.includes(i.cells.Id))
console.log(foundItems)
This should be easy to do, but I can't seem to wrap my head around it. I have a List of items that contain a shipper, receiver, order no and item id. I iterate through this list to create my Model for a View. I need to consolidate my items that have the same shipper/receiver.
Here is the code for my View:
// GET: /Hazmat/Pending
public ActionResult Pending()
{
PendingViewModel pendingViewModel = new PendingViewModel();
// Check for shipments created manually pending shipping
List<HazmatInfo> pending = hazmatRepository.GetHazmatPendingShipping().ToList();
List<PendingReceiver> pendingReceivers = new List<PendingReceiver>();
List<HazmatLocation> shippers = new List<HazmatLocation>();
PendingReceiver pendingReceiver = new PendingReceiver();
List<PendingItem> pendingItems = new List<PendingItem>();
foreach (HazmatInfo item in pending)
{
PendingReceiver tempReceiver = new PendingReceiver();
List<PendingItem> tempItems = new List<PendingItem>();
tempReceiver.Receiver = hazmatRepository.GetLocationById(item.ToBU);
tempReceiver.Shipper = hazmatRepository.GetLocationById(item.FromBU);
tempItems.Add(hazmatRepository.convertToPendingItem(hazmatRepository.GetItem(item.InvItemID), item.OrderNo, item.ToBU));
tempReceiver.PendingItems = tempItems;
pendingReceivers.Add(tempReceiver);
}
pendingReceivers = ConsolidateItems(pendingReceivers);
pendingViewModel.PendingReceivers = pendingReceivers;
//To get a distinct result, group by first found items
foreach (HazmatInfo item in pending.GroupBy(s => s.FromBU).Select(grp => grp.First()))
{
HazmatLocation shipper = new HazmatLocation();
shipper = hazmatRepository.GetLocationById(item.FromBU);
shippers.Add(shipper);
}
pendingViewModel.Shippers = shippers;
ViewBag.PendingCount = pending.Count();
return View("Pending", pendingViewModel);
}
Here is the code for consolidating my items:
private List<PendingReceiver> ConsolidateItems(List<PendingReceiver> pendingReceivers)
{
var groups = pendingReceivers.GroupBy(x => new { x.Shipper, x.Receiver });
pendingReceivers = pendingReceivers.OrderBy(s => s.Receiver.Location).ToList();
List<PendingReceiver> tempReceivers = new List<PendingReceiver>();
tempReceivers = pendingReceivers.ToList();
List<PendingItem> tempItems = new List<PendingItem>();
int i = 0;
foreach (PendingReceiver tempReceiver in pendingReceivers)
{
while (i < pendingReceivers.Count - 1)
{
if ((pendingReceivers[i].Receiver.Location == pendingReceivers[i + 1].Receiver.Location) &&
(pendingReceivers[i].Shipper.Location == pendingReceivers[i + 1].Shipper.Location))
{
if (tempItems.Count == 1)
{
tempItems.Add(pendingReceivers[i + 1].PendingItems.SingleOrDefault());
}
tempItems.Add(pendingReceivers[i].PendingItems.SingleOrDefault());
tempReceivers[i].PendingItems = tempItems;
tempReceivers.RemoveAt(i + 1);
}
i++;
}
}
return tempReceivers;
}
Here is my PendingReceivers class:
using System.Collections.Generic;
using System.Collections;
namespace Hazmat.Models
{
public class PendingReceiver : IEnumerable
{
public HazmatLocation Receiver { get; set; }
public HazmatLocation Shipper { get; set; }
public IEnumerable<PendingItem> PendingItems { get; set; }
public IEnumerator GetEnumerator()
{
yield return this.PendingItems;
}
}
}
I can picture what I need to do, but can't seem to implement it.
I've edited my ConsolidateItems method... and added more information like the data I'm using to help with this problem...
Currently, my ConsolidateItems method seems to work but skips one item (FromBu=02, ToBu=10).
Given this data:
ID OrderNo ItemID Type Qty FromBU ToBU
4055 370047528 850265 MANUAL 12.0000 24 01
4069 996564490 582526 MANUAL 1.0000 02 10
4070 996564491 940145 MANUAL 2.0000 70 49
4071 996564492 430051 MANUAL 3.0000 24 60
4072 996564493 851110 MANUAL 1.0000 02 01
4073 996564493 173000 MANUAL 10.0000 02 01
4075 996564493 928002 MANUAL 1.0000 02 01
The last 3 items in this list should be 3 items in PendingReceivers.PendingItems. My initial query pulls the above data out of my database. I need to construct my model from this data. PendingReceiver.Receiver contains my receiver data and PendingReceiver.Shipper my shipper data. I may be overthinking this too...
Thanks,
Tim
You need to group items that have the same Shipper and Receiver? You can do this with LINQ
var groups = _pendingReceivers.GroupBy(x => new { x.Shipper, x.Receiver });
This will give you an IGrouping<TKey, PendingReceiver> where TKey is the anonymous type made up of Shipper and Receiver. You can then use these groups to output into a List.
// Flatten the groups into a single list
var consolidated = groups.SelectMany(e => e);
Here it is all together, see if this works:
private List<PendingReceiver> ConsolidateItems(List<PendingReceiver> _pendingReceivers)
{
return _pendingReceivers
.GroupBy(x => new { x.Shipper, x.Receiver })
.SelectMany(e => e)
.ToList();
}
The above should work assuming you have overridden HazmatLocation.Equals correctly.
Welcome to stack overflow :)
This seems to be a stellar use case for Linq, more specifically, the GroupBy method.
Check out this article for more info
You could try this DistinctBy extention method (credit to Jon Skeet):
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source
, Func<TSource, TKey> selector)
{
var set = new HashSet<TKey>();
return source.Where(element => set.Add(selector(element)));
}
pendingReceivers = pendingReceivers.DistinctBy( pr => new { pr.Shipper
, pr.Receiver });
You will also need to implement IEqualityComparer on the Shipper and Receiver classes - http://msdn.microsoft.com/en-us/library/ms132151.aspx
I was able to take a step back to where I was doing my original query and found a LINQ statement that gave me what I'd need right off the bat instead of trying to twist the results to my will after the fact... Thanks to the help from LINQPad which I just discovered this morning.
HazmatInfo.Where (h => (h.ShippingFlag.Equals("false") && h.ShippingType.Equals("Manual")))).GroupBy(x => new { x.ToBU, x.FromBU }, y => new { y }).Distinct()
Tim
I want the query to give me back a NotificationConfiguration and also an IEnumerable<string> (which I will later transform to a single string using SB). Once the query gives me back those two items I will transform it using a constructor in my view model so I can properly display all the data using a DataTable. The answer I am looking for maybe very specific but I need to understand why I'm getting the subquery error when I want it to return an IEnumerable<string> and how to fix it. Also please note... according to the .net docs the code should be handing my back an IEnumerable<string> but for some reason its still crashing. Here is the relevant code samples again:
[HttpPost]
public ActionResult Index(DataTableRequest requestedData)
{
using (this.dataStore.Session.BeginTransaction())
{
return this.dataStore.Query<NotificationConfiguration>()
.TableRange(requestedData, p => new NotificationConfigurationViewModel(p, p.Events.Select(x => x.Code) ?? null));
}
}
.
public NotificationConfigurationViewModel(NotificationConfiguration notification , IEnumerable<string> events)
{
Contract.Requires(notification != null);
this.notification = notification;
this.events = events;
}
.
[Display(Name = "Events")]
public virtual string EventTypeCodes
{
get
{
var codes = new StringBuilder();
foreach (var item in this.events)
{
codes.Append(item + ",");
}
return codes.ToString().TrimEnd(',');
}
}
I am using the latest version Telerik MVC controls. I am using ASP.NET MVC 3 with razor.
I have a grid that lists all of my grant applications. I am wanting to use a grid that loads these grant applications via AJAX. I also need to create a client template column that has action links. These action links can vary depending on the state of each grant application.
I worked through the article at: http://gedgei.wordpress.com/2011/07/02/telerik-mvc-grid-actionlink-column/. I implemented the code as is and it works, I can create a client template column with a link in it. In my scenario I need to be able to pass in 2 parameters to the helper method, like:
column.ActionLink("Open", "Edit", "GrantApplication", item => new { id = item.Id, applicationStateId = item.GrantApplicationStateType.Id });
How I eventually implement this method in the end will change, but for now I am playing with these 2 input parameters to see how they are passed through and how I can retrieve them in the helper method.
The first question that I have regarding the article, why does the writer do the following:
var builder = factory.Template(x =>
{
var actionUrl = urlHelper.Action(action, controller, routeValues.Compile().Invoke(x));
return string.Format(#"{1}", actionUrl, linkText);
});
I can only assume that this is the server side template that is created? But nothing displays in the grid, so how do I skip this part and go directly to the client template (this is what I actually need).
The following part is also confusing because when the first parameter (id) check comes through then it is of type ParameterExpression so it goes into the true part of the if, but when the second parameter (grant application state id) comes in then it is of another type (not sure what) so then it goes into the false part of the if statement:
switch (argument.NodeType)
{
case ExpressionType.Constant:
value = ((ConstantExpression)argument).Value;
break;
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression)argument;
if (memberExpression.Expression is ParameterExpression)
value = string.Format("<#= {0} #>", memberExpression.Member.Name);
else
value = GetValue(memberExpression);
break;
default:
throw new InvalidOperationException("Unknown expression type!");
}
When the second paramenter values goes into the false part of the if statement it fails here:
value = GetValue(memberExpression);
..and gives the following error message which I have no idea what it is:
variable 'item' of type MyProject.ViewModels.GrantApplicationListViewModel' referenced from scope '', but it is not defined
Here is my view model:
public class GrantApplicationListViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullNameDisplay
{
get { return FirstName + " " + LastName; }
}
public DateTime CreatedDate { get; set; }
public GrantApplicationStateType GrantApplicationStateType { get; set; }
}
Here is my partial grid declaration in my view:
#(Html.Telerik()
.Grid<GrantApplicationListViewModel>()
.Name("grdGrantApplications")
.Columns(column =>
{
column.Bound(x => x.Id)
.ClientTemplate(
"<label class=\"reference-number\">" + "<#= Id #>" + "</label>"
)
.Title("Ref #")
.Width(70);
column.Bound(x => x.FullNameDisplay)
.Title("Owner")
.Width(200);
column.Bound(x => x.GrantApplicationStateType.Name)
.Title("Status")
.Width(90);
//column.ActionLink("Edit", "Edit", "GrantApplication", item => new { id = item.Id });
column.ActionLink("Open", "Edit", "GrantApplication", item => new { id = item.Id, applicationStateId = item.GrantApplicationStateType.Id });
})
.DataBinding(dataBinding => dataBinding.Ajax().Select("AjaxGrantApplicationsBinding", "Home"))
.Pageable(paging => paging.PageSize(30))
.TableHtmlAttributes(new { #class = "telerik-grid" })
)
What I am trying to achieve with the above is code is something to the effect of:
if grant application id = 1
then return Edit link and View link
else
then return Details link
How would I do the above? Is the code in that article the only way to do it? Isn't there a more simplar way? I did Google and couldn't find much help on what I want to do. Has any one else come across something like this?
If all you want is the client template to display different content based on the application id, it would be simpler to just put a conditional in the client template.
column.Bound(x => x.Id)
.ClientTemplate("<# if (Id == 1 ) { #> Edit Link and View Link <# } else { #> Details Link <# } #>");
The Edit, View, and Details links would be put in the same way they are put in without the conditional.
In my viewData I have an IList mls.
I want to use this to show in a dropdown. Like so:
<%= Html.DropDownList("ml3Code",
new SelectList(Model.Mls, "Code", "Description", Model.Ml3.Code ?? ""),
Model.T9n.TranslateById("Labels.All"),
new { #class = "searchInput" })%>
This works fine, until there's a myObject.Code == VOC<420 g/l.
I would have expected that an HTML helper would encode its values, but it doesn't.
How should I approach this problem? The only thing I can come up with is first making a dupe list of the objects with encoded values and then feeding it to the selectlist. This would be really bothersome.
P.S. I hope Phill H. and his team will have a long and thorough look at the encoding for asp.net-mvc 2.0...
I'm puzzled. The question "Do ASP.NET MVC helper methods like Html.DropDownList() encode the output HTML?" was asked on SO before, and the answer was "Yes" - and the source-code from the MVC framework was cited to back this assertion up.
Well, you can roll your own Html helper, but if you're like me you won't want to do that.
To me, I see two options here:
Write your select element in plain view without the helper. I've never felt the helpers provide you much save for highlighting an element when an error occurs.
Patch the select box on the client when the page loads, as in:
function encodeHtml(str)
{
var encodedHtml = escape(str);
encodedHtml = encodedHtml.replace(///g,"%2F");
encodedHtml = encodedHtml.replace(/\?/g,"%3F");
encodedHtml = encodedHtml.replace(/=/g,"%3D");
encodedHtml = encodedHtml.replace(/&/g,"%26");
encodedHtml = encodedHtml.replace(/#/g,"%40");
return encodedHtml;
}
window.onload = function()
{
var ml3Code = document.getElementById("ml3Code");
for(var i = 0; i < ml3Code.options.length; ++i)
{
ml3Code.options[i].value = encodeHtml(ml3Code.options[i].value);
}
};
It's a hack, I know. I strongly prefer the first choice.
This is encoded. But dont check with firebug - It shows values decoded.
Check in ViewSource of the Browser and things are encoded.
Controller
public List<CategoryInfo> GetCategoryList()
{
List<CategoryInfo> categories = new List<CategoryInfo>();
categories.Add(new CategoryInfo { Name = "Food<äü", Key = "VOC<420 g/l", ID = 2, Uid = new Guid("C0FD4706-4D06-4A0F-BC69-1FD0FA743B07") });
}
public ActionResult Category(ProductViewModel model )
{
IEnumerable<SelectListItem> categoryList =
from category in GetCategoryList()
select new SelectListItem
{
Text = category.Name,
Value = category.Key
};
model.CategoryList = categoryList;
return View(model);
}
View
<%= Html.DropDownList("Category" , Model.CategoryList) %>
Model
public class ProductViewModel
{
public IEnumerable<SelectListItem> CategoryList { get; set; }
public List<CategoryInfo> Categories { get; set; }
}
HTML
<select id="Category" name="Category"><option value="VOC<420 g/l">Food<äü</option>
</select>