How to pass an entire ViewModel back to the controller - asp.net-mvc

I have a ViewModel that contains two objects:
public class LookUpViewModel
{
public Searchable Searchable { get; set; }
public AddToSearchable AddToSearchable { get; set; }
}
The two contained models look something like this:
public class Searchable
{
[Key]
public int SearchableId { get; set; }
public virtual ICollection<AddToSearchable> AddedData { get; set; }
}
public class AddToSearchable
{
[Key]
public int AddToSearchableId { get; set;}
[Required]
public int SearchableId { get; set; }
[Required]
public String Data { get; set; }
[Required]
public virtual Searchable Searchable { get; set; }
}
I have a view that uses my LookUpViewModel and receives input to search for a SearchableId. If the Searchable object is found, a LookUpViewModel object is created and passed to the View. The view then displays editor fields for AddToSearchable.Data. Once submitted, I want the LookUpViewModel to be passed to an action method to handle all the back-end code. The only problem is, the LookUpViewModel passed to my action method contains a null reference to Searchable and a valid reference to AddToSearchable.. i.e. I'm missing half of my data.
Here's an example of what my view looks like:
#model HearingAidTrackingSystem.ViewModels.LookUpViewModel
#using (Html.BeginForm("LookUp", "Controller", "idStr", FormMethod.Post))
{
<input type="text" name="idStr" id="idStr"/>
<input type="submit" value="Search" />
}
#if (Model.Searchable != null && Model.AddToSearchable != null)
{
using (Html.BeginForm("AddMyStuff", "Controller"))
{
Html.HiddenFor(model => model.Searchable.SearchableId);
Html.HiddenFor(model => model.Searchable.AddedData);
Html.HiddenFor(model => model.AddToSearchable.AddToSearchableId);
Html.HiddenFor(model => model.AddToSearchable.SearchableId);
Html.HiddenFor(model => model.AddToSearchable.Searchable);
<div class="editor-field">
#Html.EditorFor(model => model.AddToSearchable.Data)
#Html.ValidationMessageFor(model => model.AddToSearchable.Data);
</div>
<input type="submit" value="Submit" />
}
}
and here are my action methods:
public ActionResult LookUp(LookUpViewModel vm)
{
return View(vm);
}
[HttpPost]
public ActionResult LookUp(string idStr)
{
int id = /*code to parse string to int goes here*/;
Searchable searchable = dal.GetById(id);
LookUpViewModel vm = new LookUpViewModel { Searchable = searchable,
AddToSearchable = new AddToSearchable() };
//When breakpoint is set, vm contains valid references
return View(vm);
}
[HttpPost]
public ActionResult AddMyStuff(LookUpViewModel vm)
{
//**Problem lies here**
//Do backend stuff
}
Sorry for the lengthy post. I tried my best to keep it simple. Any suggestions you may have.. fire away.

Two methods to fix it:
You can add to do HiddenFor() for all properties of Model.Searchable.
You can use serialization to transfer your Model.Searchable into text presentation and repair it from serialized form in controller.
Update: The problem is: You need to use #Html.HiddenFor(), not Html.HiddenFor();.

Related

ASP.NET MVC view model binding: how to populate a collection of objects?

Until some days ago it was quite easy to manage model binding in my application. I had a view model, called PersonOfferDTO, containing a collection of PersonProductOfferDTO. (yes, I'm using the DTO as a view model because a view model in this case would be equal to the DTO). Here below a simplified version of PersonOfferDTO
public class PersonOfferDTO
{
[DataMember]
public Guid PersonOfferId { get; private set; }
[DataMember]
public ICollection<PersonProductOfferDTO> Offers { get; set; }
}
And here below a simplified version of PersonProductOfferDTO
public class PersonProductOfferDTO
{
[DataMember]
public Guid PersonProductOfferId { get; private set; }
[DataMember]
public Guid PersonOfferId { get; set; }
[DataMember]
public int Quantity { get; set; }
[DataMember]
public decimal UnitPrice { get; set; }
}
I was able to populate the ICollection thanks to the method shown below (HTML code).
<form method="POST" action="/Offers/AddNewPersonOffer">
<input name="PersonProductOffers.Index" value="myKey1" hidden>
<input name="PersonProductOffers[myKey1].Quantity">
<input name="PersonProductOffers[myKey1].UnitPrice">
<input name="PersonProductOffers.Index" value="myKey2" hidden>
<input name="PersonProductOffers[myKey2].Quantity">
<input name="PersonProductOffers[myKey2].UnitPrice">
</form>
But during the last days I have increased the depth of my objects tree, so now I have the following code.
public class PersonOfferDTO
{
[DataMember]
public Guid PersonOfferId { get; private set; }
[DataMember]
public ICollection<PersonOfferParagraphDTO> Paragraphs { get; set; }
}
[DataContract]
public class PersonOfferParagraphDTO
{
[DataMember]
public Guid PersonOfferParagraphId { get; set; }
[DataMember]
public ICollection<PersonProductOfferDTO> PersonProductOffers { get; set; }
}
As you can see there is now one further level between PersonOfferDTO and PersonProductOfferDTO, and I can't figure out how to perform a "multilevel binding": create a PersonOfferDTO with more PersonOfferParagraphDTO each one containing more PersonProductOfferDTO.
NOTE: I don't want to use an incremental index ([0] , [1], ....)... but a string (["myKey"])
EDIT
By request, I add the controller here below
public ActionResult AddNewPersonOffer(PersonOfferDTO offer)
{
if (!UserHasPermissions())
{
return PartialView("_forbidden");
}
var errors = OffersCRUD.AddNewPersonOffer(offer);
if(errors.Count() == 0)
{
return RedirectToAction("Index");
}
return PartialView("_errors", new ErrorsViewModel(errors));
}
If you want to populate them with your own keys, you can define your collections within your view model as a Dictionary<string, YOURCLASS>it accepts a non-integer index value.
Example view model with Dictionary:
public class ViewModelTest
{
public Dictionary<string, Class1> Values { get; set; }
}
Example class to be used in the dictionary collection:
public class Class1
{
public int MyProperty { get; set; }
public Dictionary <string, Class2> MoreValues { get; set; }
}
public class Class2
{
public int AnotherProperty { get; set; }
}
Here's a form that populates the values:
#using (Html.BeginForm())
{
<input type="text" name="Values[yourkey1].MyProperty" />
<input type="text" name="Values[yourkey1].MoreValues[anotherKey1].AnotherProperty" />
<input type="text" name="Values[yourkey2].MyProperty" />
<input type="text" name="Values[yourkey2].MoreValues[anotherKey2].AnotherProperty" />
<input type="submit" />
}
Instead of writing your input tags yourself, you can use the helper methods and enjoy intellisense, assuming that you have your view model defined within the view with the same structure defined in your action method:
#model ViewModelTest
#using (Html.BeginForm())
{
#Html.TextBoxFor(x => x.Values[yourkey1].MyProperty)
#Html.TextBoxFor(x => x.Values[yourkey1].MoreValues[anotherKey1].AnotherProperty)
#Html.TextBoxFor(x => x.Values[yourkey2].MyProperty)
#Html.TextBoxFor(x => x.Values[yourkey2].MoreValues[anotherKey2].AnotherProperty)
<input type="submit" />
}
You'll have to introduce a view model for this of course and not just get away with using your DTO ;).
PS: A DTO shouldn't be used as a domain model either, it's for transporting information around your layers.

ASP.NET MVC 5.0 Complex Model binding

I have a view with the name "Create". This view gets the "SchoolViewModel" which contains two classes:
public class SchoolViewModel
{
public List<Teacher> ListTeacher { get; set; }
public List<SchoolClass> ListSchoolClass { get; set; }
public ClassComplete ClassComplete { get; set; }
}
Each list in "SchoolViewModel" provides data from a database.
At the "Create" page you should be able now to select a teacher and class (DropDownList). The "ClassComplete" object contains the two classes (Teacher and SchoolClass) and the roomname
public class ClassComplete
{
public string RoomName { get; set; }
public SchoolClass SchoolClass { get; set; }
public Teacher Teacher { get; set; }
}
I want only to post the "ClassComplete" object.
My ActionResult
[HttpPost]
public ActionResult Create(ClassComplete cp)
{
// Do something
return View();
}
Edit:
Razor View
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.ListTeacher[0].TeacherName)
#Html.EditorFor(m => m.ListSchoolClass[0].ClassName)
#Html.TextBoxFor(m => m.cl.RoomName)<br />
<input type="submit" value="Click" />
}
Is this the right way ?
best regards
If you want to POST only ClassComplete model you will need to indicate the binding prefix:
[HttpPost]
public ActionResult Create([Bind(Prefix="ClassComplete")] ClassComplete cp)
{
// Do something
return View();
}
and in your view:
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.ClassComplete.RoomName)
<br />
<input type="submit" value="Click" />
}
The TextBoxFor will generate the following input field in the resulting markup:
<input type="text" name="ClassComplete.RoomName" />
Notice the name of the input field. That's the reason why you need to indicate this prefix in your controller action.
This will also work for the other properties if you want to send them you just need to include the corresponding input fields:
#Html.TextBoxFor(m => m.ClassComplete.SchoolClass.SomeProperty)
#Html.TextBoxFor(m => m.ClassComplete.Teacher.SomeOtherProperty)
...

Html.DropDownListFor returns null

There are similar questions and I have tried most of them. Instead of continuing to destroy the remaining code while conjuring demons from the similar past questions, I have decided to ask for help.
When I arrive at the AddMembership action I get a null value instead of the selected item. Below are the details.
View;
#using (Html.BeginForm("AddMembership", "WorkSpace", FormMethod.Post, new { data_ajax = "true", id = "frmAddMembership" }))
{
<div id="newMembershipDiv">
#Html.DropDownListFor(m => m.selectedID, new SelectList(Model.allInfo, "Value","Text",1), "Select!")
<input type="submit" value="Add" name="Command" />
</div>
}
Controller (I just want to see the selectedID or anything appear here.);
public ActionResult AddMembership(SelectListItem selectedID)
{
return View();
}
Model;
public class SomeModel
{
public SelectList allInfo { get; set; }
public SelectListItem selectedID { get; set; }
}
The Monstrosity which initializes the allInfo SelectList
model.allInfo = new SelectList(synHelper.getall().ToArray<Person>().Select(r => new SelectListItem {Text=r.Name, Value=r.prID.ToString() }));
synHelper.getAll() returns a List of the below class;
public class Person
{
public Guid prID { get; set; }
public string Name { get; set; }
}
The only thing that is posted to your action is a selectedID, which is a simple string. If you wander, in the request it looks as simple as:
selectedID=1234
Therefore it should be awaited for as a simple string. Adjust your action parameter:
public ActionResult AddMembership(string selectedID)

ASP.NET MVC 4 Navigation Virtual Property not populated on Post Action

I have a navigation property (Category) on a Question class for which I am manually creating a DropDownList for in the Create view of Question, and when posting the Create action, the Category navigation property is not populated on the Model, therefore giving me an invalid ModelState.
Here is my model:
public class Category
{
[Key]
[Required]
public int CategoryId { get; set; }
[Required]
public string CategoryName { get; set; }
public virtual List<Question> Questions { get; set; }
}
public class Question
{
[Required]
public int QuestionId { get; set; }
[Required]
public string QuestionText { get; set; }
[Required]
public string Answer { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Category { get; set; }
public int CategoryId { get; set; }
}
Here is my Question controller for both GET and POST actions of Create:
public ActionResult Create(int? id)
{
ViewBag.Categories = Categories.Select(option => new SelectListItem {
Text = option.CategoryName,
Value = option.CategoryId.ToString(),
Selected = (id == option.CategoryId)
});
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Question question)
{
if (ModelState.IsValid)
{
db.Questions.Add(question);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(question);
}
And here is the Create view for Question
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Question</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Category)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.Category.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
</div>
<div class="editor-label">
#Html.LabelFor(model => model.QuestionText)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.QuestionText)
#Html.ValidationMessageFor(model => model.QuestionText)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Answer)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Answer)
#Html.ValidationMessageFor(model => model.Answer)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
I have tried the following variations of generating the dropdownlist on the view:
#Html.DropDownListFor(model => model.Category.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
#Html.DropDownListFor(model => model.Category, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
#Html.DropDownList("Category", (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
#Html.DropDownList("CategoryId", (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
When I quickwatch the Question object on the POST action, the Category property is null, but the CategoryId field on the property is set to the selected Category on the view.
I know I could easily add code to manually fetch the Category with EF by using the CategoryId value that I get from the view. I also think I could create a custom binder to do this, but I was hoping that this could be done with data annotations.
Am I missing something?
Is there a better way to generate the dropdownlist for the navigation property?
Is there a way to let MVC know how to populate the navigation property without me having to manually do it?
-- EDIT:
If it makes any difference, I do not need the actual navigation property loaded when creating/saving the Question, I just need the CategoryId to be correctly saved to the Database, which isn't happening.
Thanks
Instead of
#Html.DropDownListFor(model => model.Category.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
Try
#Html.DropDownListFor(model => model.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
Edit:
There is no automatic way to populate Navigation property from the id posted from the form. Because, a database query should be issued to get the data and it should not be transparent. It should be done explicitly. Moreover, doing this operation in a custom binder probably probably is not the best way. There is a good explanation in this link : Inject a dependency into a custom model binder and using InRequestScope using Ninject
I know this question is already answered, but it got me thinking.
So I think I found a way of doing this with some conventions.
First, I made the entities inherit from a base class like this:
public abstract class Entity
{
}
public class Question : Entity
{
[Required]
public int QuestionId { get; set; }
[Required]
public string QuestionText { get; set; }
[Required]
public string Answer { get; set; }
public virtual Category Category { get; set; }
}
public class Category : Entity
{
[Key]
[Required]
public int CategoryId { get; set; }
[Required]
public string CategoryName { get; set; }
public virtual List<Question> Questions { get; set; }
}
So, I also changed the Question model to not have an extra property called CategoryId.
For the form all I did was:
#Html.DropDownList("CategoryId", (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
So here's the second convention, you'd have to have a property field be named with an Id suffix.
Finally, the CustomModelBinder and CustomModelBinderProvider
public class CustomModelBinderProvider : IModelBinderProvider
{
private readonly IKernel _kernel;
public CustomModelBinderProvider(IKernel kernel)
{
_kernel = kernel;
}
public IModelBinder GetBinder(Type modelType)
{
if (!typeof(Entity).IsAssignableFrom(modelType))
return null;
Type modelBinderType = typeof (CustomModelBinder<>)
.MakeGenericType(modelType);
// I registered the CustomModelBinder using Windsor
return _kernel.Resolve(modelBinderType) as IModelBinder;
}
}
public class CustomModelBinder : DefaultModelBinder where T : Entity
{
private readonly QuestionsContext _db;
public CustomModelBinder(QuestionsContext db)
{
_db = db;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext) as T;
foreach (var property in typeof(T).GetProperties())
{
if (property.PropertyType.BaseType == typeof(Entity))
{
var result = bindingContext.ValueProvider.GetValue(string.Format("{0}Id", property.Name));
if(result != null)
{
var rawIdValue = result.AttemptedValue;
int id;
if (int.TryParse(rawIdValue, out id))
{
if (id != 0)
{
var value = _db.Set(property.PropertyType).Find(id);
property.SetValue(model, value, null);
}
}
}
}
}
return model;
}
}
The CustomModelBinder will look for properties of type Entity and load the data with the passed Id using EF.
Here I am using Windsor to inject the dependencies, but you could use any other IoC container.
And that's it. You have a way to make that binding automagically.

ASP .NET MVC4 Adding new items to view and model binding

I create a website for my wife. She's a teacher and she would like to have a possibility to create exercises for their students. The case is that she would like to create for instance the following exercise:
Exercise 1: Fill the sentence using a correct word:
My wife is 30 ............. old
I live in this city for 30 .........
I have the following model:
public class Exercise
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ExerciseType Type { get; set; }
public DifficulityLevel DifficulityLevel { get; set; }
public List<ExerciseItem> Items { get; set; }
public DateTime TimeOfCreation { get; set; }
public DateTime TimeOfModification { get; set; }
}
public class ExerciseItem
{
[Key]
public Guid Id { get; set; }
public string Content { get; set; }
public List<ExerciseItemOption> Options { get; set; }
public ExerciseItemOption CorrectSelection { get; set; }
}
I creates a View for my Exercise. I can fill in the basic properties like Name, Description, Difficulity Level and Type. Then I would like to create a button "Add exercise item". When clicked, a partial view (or something else) should be added dynamically where new ExerciseItem can be provided.
I've tried to following:
I've added a button
#Ajax.ActionLink("Add exercise item",
"AddExerciseItem",
"Exercise", new AjaxOptions() { HttpMethod="GET", InsertionMode = InsertionMode.InsertBefore, UpdateTargetId="ExerciseItems"})
and the appropriate div:
<div id="ExerciseItems"></div>
My action method looks as follows:
public ActionResult AddExerciseItem()
{
return PartialView("ExerciseItem", new ExerciseItem());
}
and the partial view:
#model ElangWeb.Models.ExerciseItem
<fieldset>
<legend>ExerciseItem</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.DisplayNameFor(model => model.Content)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Content, new { style = "width:200px" })
</div>
</fieldset>
It works fine. However when I click button for creating a whole exercise, I do not have ExerciseItem collection in my model:
public ActionResult Create(Exercise exercise)
{
using (PersistanceManager pm = new PersistanceManager())
{
exercise.Id = Guid.NewGuid();
exercise.TimeOfCreation = DateTime.Now;
exercise.TimeOfModification = DateTime.Now;
pm.ExcerciseRepository.Add(exercise);
}
return RedirectToAction("Index");
}
How should I change the code in order to bind my list of added ExerciseItem objects to my model Exercise?
Check out this article about model binding. You basically need to create special names for the exercise items so that they get bound correctly.
e.g. partial:
#model ElangWeb.Models.ExerciseItem
<fieldset>
<legend>ExerciseItem</legend>
<label>content</label>
<input type="hidden" name="ExcersiseItem.Index" value="SomeUniqueValueForThisItem" />
<input type="text" name="ExcersiseItem[SomeUniqueValueForThisItem].Name" value="#Model.Content" />
</fieldset>
You can also look at my answer to this question MVC3 Non-Sequential Indices and DefaultModelBinder. Thanks Yarx for finding it, I was actually trying to find it :)

Resources