MVC3- Passing an entire model with dynamic data from view to controller - asp.net

I have two models: OuterModel and InnerModel. There is a one to many relationship between OuterModel and InnerModel, respectively. To clarify my question, my model is of type IEnumerable<OuterModel>. I'm passing a random number of OuterModels to the view and the user creates any number of InnerModels for each OuterModel. Then on submission, I want the controller to receive the list of OuterModels so that the InnerModels can be added to the database to their intended OuterModels.
I believe I have the naming convention correct to make use of MVC's built in model binding. Here's what that looks like:
OuterModel[i].InnerModel[j].Property
My problem is, I don't really know how to get a list of OuterModels passed to the controller. Here's what I've tried in my View:
#model IEnumerable<OuterModel>
#using (Html.BeginForm("Create", "Controller", new { OuterModels = Model }, FormMethod.Post))
{
//Code to create the InnerModels here
}
And here's what I have in my Controller:
[HttpPost]
public ActionResult Create(IEnumerable<OuterModel> OuterModels, FormCollection fc)
{
String[] keys = fc.AllKeys;
if(ModelState.IsValid){
//Add to db
}
}
Keys shows that all of my properties are following the naming convention that I specified earlier, but ModelState.IsValid is returning false. It shows that OuterModels' count is 0.
Even though I'm telling the form to submit OuterModels = Model before any InnerModels are created, you would think there would still be data in OuterModels considering it's passed to the view. I am really tired today, so I'm guessing I'm looking over one (or many) small detail(s). Any suggestions?
--EDIT--
Passing a list of OuterModels to the controller may not be the best approach. If anybody has a better suggestion, please share.

As long as indexes are used properly, then this should not be an issue. Here is how I would envision the form names.
Model[0].foo
Model[0].Inner[0].bar
Model[0].Inner[1].bar
Where outer model has a property called foo and
Outer model has a property called inner which is a collection of inner objects. Inner object has a property called bar. If your form is rendered with the correct indexes then the model binding should work. Things can get tricky if form fields are generated client side. I recommended going back to server in order to manipulate the model. There are some extra round trips, but you can make them via Ajax request.
Here are some more details in a more fleshed out example.
public class InnerModel{
public string Name{get; set;}
}
public class OuterModel{
public List<InnerModel> InnerModels{get; set;}
public string Name{get; set;}
}
Here is what I would envision my view would look like:
#model IEnumerable<OuterModel>
<ul>
#{int i = 0;}
#foreach(var item in Model){
<li>
Outer Name : #Html.TextBoxFor(m=>Model[i].Name)
<br />
#{int j = 0;}
<ul>
#foreach(var innerItem in Model[i].InnerModels){
<li>Inner Name : #Html.TextBoxFor(m=> Model[i].InnerModels[j].Name)</li>
j++;
}
</ul>
i++;
</li>
}
</ul>
If this is wrapped in a form--- and the controller action looks like this:
public ActionResult Action(List<OuterModel> model){
}
then I would think model would be populated correctly.
I noticed your form.. it doesn't look right to me... I wouldn't think that the passing the OuterModels like that is going to work-- although frankly I might be wrong.
#using (Html.BeginForm("Create", "Controller", new { OuterModels = Model }, FormMethod.Post))
{
//Code to create the InnerModels here
}
Here is an example I did for the class I teach.. that definitely works..
public class Author
{
public string Name { get; set; }
}
public class Book
{
public string Name { get; set; }
public List<Author> Authors { get; set; }
}
Controller:
public class BookController : Controller
{
public static List<Book> _model = null;
public List<Book> Model
{
get
{
if (_model == null)
{
_model = new List<Book>
{
new Book{
Name = "Go Dog Go",
Authors = new List<Author>{
new Author{Name = "Dr. Seuss"}
}},
new Book{
Name = "All the Presidents Men",
Authors = new List<Author>{
new Author{Name = "Woodward"},
new Author{Name = "Bernstein"}
}},
new Book{
Name = "Pro ASP.NET MVC Framework",
Authors = new List<Author>{
new Author{Name = "Sanderson"},
new Author{Name = "Stewart"},
new Author {Name = "Freeman"}
}}
};
}
return _model;
}
}
public ActionResult Index()
{
return View(Model);
}
public ActionResult Edit()
{
return View(Model);
}
[HttpPost]
public ActionResult Edit(List<Book> books)
{
_model = books;
return RedirectToAction("Index");
//return View(books);
}
}
and View:
#model List<AmazonWeb.Models.Book>
#{
ViewBag.Title = "Index";
}
<div class="content">
#Html.ActionLink("Index", "Index")
#using (Html.BeginForm())
{
<input type="submit" value="save" />
<ul class="book-list">
#for (var i = 0; i < Model.Count; i++ )
{
<li>
<label>Book Name</label> : #Html.TextBoxFor(m => Model[i].Name)
<ul>
#for (var j = 0; j < Model[i].Authors.Count; j++ )
{
<li><label>Author Name</label> : #Html.TextBoxFor(m => Model[i].Authors[j].Name)</li>
}
</ul>
</li>
}
</ul>
<input type="submit" value="save" />
}
</div>

Related

EditorForMany not working for objects deeper than 1 level

I have an example asp.net mvc5 program in which I'm trying to build a payment model with many levels of partials added to make a complete object. In this example, I am using generic data. I have a top level 'testing', to which you can add multiple 'A1' objects, and to that you can add multiple 'B2' objects.
The form uses ajax and jqueryto allow the person to add data on the fly, which is then submitted all at once when the submit button is pressed.
I found an html helper made by Matt Lunn that does an editorForMany. It works very well, adds all my info to the web page, but it will never post back a model that is deeper than 2 levels(top, with a1's attached).
I can get the entire model to build on my page. It looks appropriate, but when I post back, nothing under A1 shows up. I can add as many 'A1's as I want. If I change the code and put 'B2's directly under testing, that will work, but nothing will add under the 'A1's as I have it.
Here is my code. I apologize for the formatting and the length of this post.
MVC helper
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, bool includeIndexField = true) where TModel : class
{
var items = propertyExpression.Compile()(html.ViewData.Model);
var htmlBuilder = new StringBuilder();
var htmlFieldName = ExpressionHelper.GetExpressionText(propertyExpression);
var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName);
Func<TValue, string> indexResolver = null;
if (indexResolverExpression == null)
{
indexResolver = x => null;
}
else
{
indexResolver = indexResolverExpression.Compile();
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = indexResolver(item);
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters);
if (String.IsNullOrEmpty(guid))
{
guid = Guid.NewGuid().ToString();
}
else
{
guid = html.AttributeEncode(guid);
}
if (includeIndexField)
{
htmlBuilder.Append(_EditorForManyIndexField<TValue>(htmlFieldNameWithPrefix, guid, indexResolverExpression));
}
htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
}
MvcHtmlString m1 = new MvcHtmlString(htmlBuilder.ToString());
return m1;
}
testing Controller
public class testingController : Controller
{
// GET: testing
public ActionResult startTest()
{
var model = new testing();
return View(model);
}
[HttpPost]
public ActionResult startTest([Bind] testing model)
{
if (ModelState.IsValid)
{
var r = 1;
}
return View(model);
}
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult addA1()//current running test
{
var model = new testing();
model.aas.Add(new A1());
return View(model);
}
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult addB2()//current running test
{
var model = new A1();
model.bbs.Add(new B2());
return View(model);
}
}
model class
public class testing
{
public string name { get; set; }
public List<A1> aas { get; set; }
public testing()
{
aas = new List<A1>();
}
}
public class A1
{
public string aName { get; set; }
public List<B2> bbs { get; set; }
public A1()
{
bbs = new List<B2>();
}
}
public class B2
{
public string bName { get; set; }
public B2() { }
}
startTest.cshtml
#model proofOfConceptPaymentBuilder.Models.testing
#{
ViewBag.Title = "startTest";
}
#section Scripts
{
<script>
jQuery(document).ready(function ($) {
$('#add-bbs').on('click', function () {
jQuery.get('/testing/addB2').done(function (html) {
$('#bbsList').append(html);
});
});
$('#add-aas').on('click', function () {
jQuery.get('/testing/addA1').done(function (html) {
$('#aasList').append(html);
});
});
});
function alertSomething() {
alert('something');
jQuery.get('/testing/addB2').done(function (html) {
$('#bbsList').append(html);
});
};
</script>
}
#using (Html.BeginForm())
{
<h2>Create</h2>
#Html.EditorFor(x => x)
<input type="submit" />
}
EditorTemplates
(testing editor template)
#model proofOfConceptPaymentBuilder.Models.testing
<div class="form-group">
#Html.LabelFor(x => x.name)
#Html.EditorFor(x => x.name)
</div>
<div class="form-group">
<div id="aasList">
#Html.EditorFor(x => x.aas)
</div>
<input type="button" id="add-aas" value="add aas" />
<input type="button" id="delete-testing" value="delete test" />
</div>
(a1 editor template)
#model proofOfConceptPaymentBuilder.Models.A1
<div class="form-group">
#Html.LabelFor(x => x.aName)
#Html.EditorFor(x => x.aName)
</div>
<div class="form-group">
<div id="bbsList">
#Html.EditorForMany(x => x.bbs)
</div>
<input type="button" id="add-bbs" value="add bss" onclick="alertSomething()"/>
<input type="button" id="delete-aas" value="delete ass" />
</div>
(b2 editor template)
#model proofOfConceptPaymentBuilder.Models.B2
<div class="form-group">
#Html.LabelFor(x => x.bName)
#Html.EditorFor(x => x.bName)
</div>
<div>
<input type="button" id="delete-bss" value="delete bbs" />
</div>
addA1.cshtml
#model proofOfConceptPaymentBuilder.Models.testing
#{
Layout = null;
}
#Html.EditorForMany(x => x.aas)
addB2.cshtml
#model proofOfConceptPaymentBuilder.Models.A1
#{
Layout = null;
}
#Html.EditorForMany(x => x.bbs)
Ok, so I'm sorry for the formatting. I'm kinda new to the formatting here. I had to manually indent my code for it to show up in code blocks.
I also stumbled upon this problem and, given the number of questions I have found here in StackOverflow dealing with similar problems, I have created a .NET Core library to solve exactly this. It is called DynamicVML (Dynamic View-Model Lists) and you can get it with NuGet.
It is basically a list templating engine for ASP.NET Core, that you can use to display lists of view models of any depth.
You can use it like this:
#Html.ListEditorFor(x => x.Addresses,
Url.Action("AddAddress"), // the action in your controller that creates views
"Add new address", // some text for the "add new item button" in your form
listContainerTemplate: "viewThatWrapsTheList",
listTemplate: "viewForTheList",
itemContainerTemplate: "viewThatWrapsYourViewModel",
ItemTemplate: "viewForYourViewModel")
Now, you do not actually have to specifiy all of this. If you want you can do just
#Html.ListEditorFor(x => x.Addresses, Url.Action("AddAddress"), "Add new address")
And it will guess all the rest.
It will support nesting with any depth, as it was the core of the question.

Multiple models in same view in asp.net mvc 4

I'm using asp.net mvc 4 & Entity Framework 6 to make a website where after user login, the table records will show & the ID of the user will show as Session. But when I run, after login, I get this error, Object reference not set to an instance of an object. I've made a custom model where two DbSet from seperate EF6 models are attached. My code is below,
Custom Model
public class MkistatVsUserLogin
{
public sample_1 sample_1 { get; set; } //Login Model
public IEnumerable<mkistat> mkistats { get; set; } //Table Model
}
Controller
[HttpPost]
public ActionResult Login(sample_1 id)
{
if (ModelState.IsValid)
{
var uservar = db.sample_1.Where(a => a.boid.Equals(id.boid)).FirstOrDefault();
if (uservar != null)
{
Session["UserBOID"] = uservar.boid.ToString();
return RedirectToAction("UserLogin");
}
}
var mkimodel = new MkistatVsUserLogin { mkistats = dsedb.mkistats.ToList() };
return View(id);
return View(mkimodel);
}
View
#model ABCoLtd.Models.MkistatVsUserLogin
#if (Session["UserBOID"] != null)
{
<li>Welcome, <b>#Session["UserBOID"].ToString()</b></li>
}
<a class="btn btn-default" href="#Url.Action("UserLogout", "Home")">Log Out</a>
#foreach (var item in Model.mkistats)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.MKISTAT_ID)
</td>
<td>
#Html.DisplayFor(modelItem => item.MKISTAT_PHONE_NO)
</td>
</tr>
}
Is there something wrong with my code? If it is then please give me a solution. All I want is to use both models at the same view where user login model will be used in Session & table model to list all the records.
Remove bellow line
return View(id);
and in your return View line also mention View or ActionMethod name
return View("ViewName",mkimodel)
In your action method, Use Session to pass ID
Session["UserID"] = id;
You can use the same in your View as :
<html>
-----Your CODE
<body>
------Your CODE
#Session["UserID"]
------Your CODE
</body>
</html>
You can create a ViewModel to handle :
namespace ABCoLtd.ViewModels
{
public class LoginVM
{
public MkistatVsUserLogin MkistatVsUserLogin {get;set;}
public int Id {get;set;}
}
}
and in action pass this way:
var mkimodel = new MkistatVsUserLogin { mkistats = dsedb.mkistats.ToList() };
LoginVM vm = new LoginVM();
vm.MkistatVsUserLogin = mkimodel ;
vm.Id = id;
return View(vm );
and in View set model to LoginVM:
#model ABCoLtd.ViewModels.LoginVM
UPDATE:
you already have a viewmodel didnt noticed that, you just need to do this:
var mkimodel = new MkistatVsUserLogin
{
mkistats = dsedb.mkistats.ToList(),
sample_1 = id
};
return View(nkimodel);
After login you are directly redirecting your user to an action and that action you have not mentioned here.And that action whose view you have shown is an strongly typed view which expects a list of MkistatVsUserLogin. But, I think you are not passing that list, so Model property of view will remain null and that will be causing an error of object reference. Please check this issue.
I got it. I just made another method in controller for viewing table with user session.
[HttpPost]
public ActionResult Login(sample_1 id)
{
if (ModelState.IsValid)
{
var uservar = db.sample_1.Where(a => a.boid.Equals(id.boid)).FirstOrDefault();
if (uservar != null)
{
Session["UserBOID"] = uservar.boid.ToString();
return RedirectToAction("UserLogin");
}
}
return View(id);
}
public ActionResult UserLogin()
{
if(Session["UserBOID"] != null)
{
var mkimodel = new MkistatVsUserLogin { mkistats = dsedb.mkistats.ToList() };
return View(mkimodel);
}
else
{
return RedirectToAction("Login");
}
}

Asp.net MVC 3: Model comes empty in the controller?

I've a form in which I only have to choose between several entities.
So here is what I've done:
Created a view Model:
public class EntityChooserModel
{
public IEnumerable<Entity> AvailableEntities { get; set; }
public int SelectedId;
}
I've two actions in the controller, one with [HTTPPost], one without. In both I have a "SessionUserData", which is session data, bound through data model binding.
[Authorize]
public ActionResult EntityChooser(SessionUserData sessionData)
{
List<Entity> entities = _userStore.GetRoles(User.Identity.Name).Select(ur => ur.Entity).ToList();
Entity entity = sessionData.IdCurrentEntity != null ? entities.Where(e => e.Id == sessionData.IdCurrentEntity).First() : entities.First();
return View(new EntityChooserModel() { SelectedId = entity.Id, AvailableEntities = entities });
}
.
[Authorize]
[HttpPost]
public ActionResult EntityChooser( EntityChooserModel model, SessionUserData sessionData, String returnUrl)
{
if (ModelState.IsValid)
{
Entity entity = _userStore.GetRoles(User.Identity.Name).Select(ur => ur.Entity).Where(e => e.Id == model.SelectedId).First();
sessionData.IdCurrentEntity = entity.Id;
RedirectToDefaultPage(returnUrl);
}
return View(model);
}
And the strongly typed view:
#model My.Full.Namespace.EntityChooserModel
#{
ViewBag.Title = "EntityChooser";
Layout = "~/Views/Shared/_LoginLayout.cshtml";
}
<div id="login-wrapper">
<div id="title">
<h1>Login</h1>
</div>
<div id="login">
#using (Html.BeginForm("EntityChooser", "Auth", FormMethod.Post, new { id = "ChooseFormId" }))
{
#Html.ValidationSummary(true, "Loggin error")
<div>
<span>Choose an entity:</span>
#Html.DropDownListFor(x => x.SelectedId, new SelectList(Model.AvailableEntities, "Id", "Name"))
</div>
<p class="right-aligned">
<a href="javascript:document.getElementById('ChooseFormId').submit();" class="adm-button">
<span class="next-btn"><span>Select</span></span></a>
</p>
<input type="submit" style="display: none" />
}
</div>
</div>
The problem is that when I receive the data in the second model, I've an empty object. The collection is empty, and the id is set to the default value(0).
What is wrong? I can't find out what I missed on this one.
It's the first time I do an mvc form which isn't ForModel(), and the first time I've to use a dropdownlist
The only input element that your <form> contains is a single drop down list which sends only the selected value to the server when POSTed. This explains why your AvailableEntities collection property is empty.
As far as why the SelectedId field is empty, it is because you have defined it as a field and not as a property. The default model binder works only with properties. So if you want it to be populated you must define it as a property:
public int SelectedId { get; set; }
As far as the AvailableEntities property is concerned you could populate it from your repository in the POST action using the selected id.

How can I bind a list of lists to a view

I have a ViewModel that have a list object containing other list objects and want to bind that to a view.
What I have done so far is to loop through the first list like so:
for (int i = 0; i < Model.ThFirstList.Count(); i++ )
{
Html.EditorFor(model => model.TheFirstList[i] , "myView")
}
And in "myView" a loop through the other list like so:
for (int i = 0; i < Model.TheSecondList.Count(); i++ )
{
%><%: Html.DropDownListFor(m => m.TheSecondList[i].ThePropertyIWantToSet, aList)%><%
}
Everything looks fine and on the client side (I think) and the name of the select-fields is like this:
TheFirstList[0].TheSecondList[0].ThePropertyIWantToSet
When I don't choose anything from any of the select boxes and post back the data then TheFirsList and TheSecendList objects is set. However if I choose a value in the list I get Error Code 500 back from the server. My guess is that it can't find any matching Controller method for the request? Because of this I can't set the ThePropertyIWantToSet property.
Can anyone help me on this?
Thanks!
Instead of bothering with foreach loops etc, I would simply use a view model and editor templates. It makes life easier.
So start with a view model:
public class MyViewModel
{
public IEnumerable<Type1> TheFirstList { get; set; }
}
public class Type1
{
public IEnumerable<Type2> TheSecondList { get; set; }
}
public class Type2
{
public string ThePropertyIWantToSet { get; set; }
public IEnumerable<SelectListItem> Items
{
get
{
return new[]
{
new SelectListItem { Value = "1", Text = "item 1" },
new SelectListItem { Value = "2", Text = "item 2" },
new SelectListItem { Value = "3", Text = "item 3" },
};
}
}
}
then a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
// filling with dummy values => those should probably
// come from your data store
var model = new MyViewModel
{
TheFirstList = Enumerable.Range(1, 2).Select(x => new Type1
{
TheSecondList = Enumerable.Range(1, 3).Select(y => new Type2())
})
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
Then corresponding ~/Views/Home/Index.cshtml view:
#model MyViewModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.TheFirstList)
<input type="submit" value="OK" />
}
The ~/Views/Home/EditorTemplates/Type1.cshtml template:
#model Type1
#Html.EditorFor(x => x.TheSecondList)
and finally the ~/Views/Home/EditorTemplates/Type2.cshtml template:
#model Type2
#Html.DropDownListFor(
x => x.ThePropertyIWantToSet,
new SelectList(Model.Items, "Value", "Text")
)
Now in the POST action you will get a nice and properly bound view model. Editor templates will take care of generating the correct input field names. See how easy and clean the code is. No need of writing any loops in the views, worrying about proper indexes, ... just leave this to the framework.
Seeing your view model may be more helpful, but typically when rendering select lists from a view model I use two separate properties: one to store the selected value and the other to actually hold the list itself.
ViewModels with SelectList Design Decison

LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method cannot be translated into a store expression

This is my controller class
public class HomeController : Controller
{
private rikuEntities rk = new rikuEntities();
public ActionResult Index()
{
var db = new rikuEntities();
IEnumerable<SelectListItem> items = db.emp.Select(c => new
SelectListItem
{
Value = c.Id.ToString(),
Text = c.name
});
ViewBag.CategoryID = items;
return View();
}
}
this is my view
#using (Html.BeginForm("viewToController", "Home"))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>emp</legend>
<div class="editor-field">
#Html.DropDownList("CategoryID", (IEnumerable<SelectListItem>) ViewBag.Categories)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
whenever I run this program I get this error:
LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method cannot be translated into a store expression." in the statement #Html.DropDownList("CategoryID", (IEnumerable) ViewBag.Categories). i am using entity framework mechanism for databse connection. please help me to find out the error...
I would recommend you to use view models and strongly typed views instead of ViewBag. So start with defining your view model:
public class EmployeeViewModel
{
public string CategoryId { get; set; }
public IEnumerable<Employee> Categories { get; set; }
}
then in the controller populate this view model:
public class HomeController : Controller
{
public ActionResult Index()
{
var db = new rikuEntities();
var model = new EmployeeViewModel
{
Categories = db.emp.ToArray() // <-- you probably want categories here
};
return View(model);
}
}
and in the view:
#model EmployeeViewModel
#using (Html.BeginForm("viewToController", "Home"))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>emp</legend>
<div class="editor-field">
#Html.DropDownListFor(
x => x.CategoryId,
new SelectList(Model.Categories, "Id", "name")
)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Sadly EF does not know how to convert ToString() to an SQL statement.
You must, therefore, use the embedded function SqlFunctions.StringConvert.
There is no overload for int so you will need to typecast to double :-(
var items = from v in db.emp
select new SelectListItem
{
Text = c.name,
Code = SqlFunctions.StringConvert((double)c.Id)
};

Resources