I have two related POCOs
public class Parent
{
public Guid Id {get; set;}
public IList<Child> ChildProperty {get; set;}
}
public class Child
{
public Guid Id {get; set;}
public String Name {get; set;}
}
and I have a .cshtml Razor view with
<div>
#{
var children =
new SelectList(Child.FindAll(), "Id", "Name").ToList();
}
#Html.LabelFor(m => m.Child)
#Html.DropDownListFor(m => m.Child.Id, , children, "None/Unknown")
</div>
I'd like to do the following in my controller class:
[HttpPost]
public ActionResult Create(Parent parent)
{
if (TryUpdateModel(parent))
{
asset.Save();
return RedirectToAction("Index", "Parent");
}
return View(parent);
}
Such that if the user selects "None/Unknown", the child value of the parent object in the controller is null but if the user selects any other value (i.e. an ID of a child object retrieved from the database), the child value of the parent object is instantiated and populated with that ID.
Basically I'm struggling with how to persist a list of possible entities across the HTTP stateless boundary such that one of the entities is properly rehydrated and assigned via the default model binder. Am I just asking for too much?
Am I just asking for too much?
Yes, you are asking for too much.
All that's sent with the POST request is the ID of the selected entity. Don't expect to get much more than that. If you want to rehydrate or whatever you should query your database. The same way you did in your GET action to populate the child collection.
Oh and there's a problem with your POST action => you are calling the default model binder twice.
Here are the 2 possible patterns (personally I prefer the first but the second might be useful in some situations as well when you want to manually call the default model binder):
[HttpPost]
public ActionResult Create(Parent parent)
{
if (ModelState.IsValid)
{
// The model is valid
asset.Save();
return RedirectToAction("Index", "Parent");
}
// the model is invalid => we must redisplay the same view.
// but for this we obviously must fill the child collection
// which is used in the dropdown list
parent.ChildProperty = RehydrateTheSameWayYouDidInYourGetAction();
return View(parent);
}
or:
[HttpPost]
public ActionResult Create()
{
var parent = new Parent();
if (TryUpdateModel(parent))
{
// The model is valid
asset.Save();
return RedirectToAction("Index", "Parent");
}
// the model is invalid => we must redisplay the same view.
// but for this we obviously must fill the child collection
// which is used in the dropdown list
parent.ChildProperty = RehydrateTheSameWayYouDidInYourGetAction();
return View(parent);
}
In your code you've made some mix of the two which is wrong. You are basically invoking the default model binder twice.
Related
suppose I have the following class
class A
{
[Required]
public string Name {get; set;}
[Required]
public string NickName {get; set;}
[Required]
public string UserId {get; set;}
}
and from the form I am passing only the Name and NickName to controller and before checking the model state simply I assign the user id to the UserId property as below
[HttpPost]
public IActionResult Save(A model)
{
model.UserId = User.GetLoggedInUserId<string>();
if (!ModelState.IsValid)
{
return View(model);
}
}
even though I have assign the user id before checking the model state it still returns the validation state false and complaining the for the user id. one way to come out of this problem is to create a view model which makes things more complex, because of assigning the values from view model to class it self.
any idea how to solve this.
Note: the question is not only for the User Id in the class there maybe other properties as well that may not be passed from the form to controller and the values maybe assigned to them from controller
You could try to remove the 'UserId' from the model validation before calling ModelState.IsValid. Code like this:
[HttpPost]
public IActionResult CreateA(A a)
{
var state1 = ModelState.IsValid; // false
ModelState.Remove("UserId"); //using Remove method to remove the specified object from the model-state dictionary.
var state2 = ModelState.IsValid; // true
a.UserId = "SN1001";
if (ModelState.IsValid)
{
var data = a.UserId;
}
return RedirectToAction(nameof(Index));
}
The screenshot as below:
Besides, you could also try to use the TryValidateModel() method to validate the model again in the controller, code like this:
[HttpPost]
public IActionResult CreateA(A a)
{
var state1 = ModelState.IsValid; // false
ModelState.Remove("UserId");
a.UserId = "SN1001";
if (!TryValidateModel(a, nameof(a)))
{
var state2 = ModelState.IsValid;
}
if (ModelState.IsValid)
{
var data = a.UserId;
}
return RedirectToAction(nameof(Index));
}
The result like this:
Reference: Model State Rerun validation
Edit
[Note] If the ModelState.IsValid is false first, before rerun validation using the TryValidateModel method, we have to remove the error from the ModelState.
You can also pass the value of UserId field using a hidden field with a default value, like:
#Html.HiddenFor(m => m.UserId, new { #Value = User.Identity.Name });
This answer might help What does ModelState.IsValid do?
ModelState.IsValid indicates if it was possible to bind the incoming values from the request to the model correctly and whether any explicitly specified validation rules were broken during the model binding process.
There is no way to solve this. If you don't need this validation, then remove it altogether with the attributes and add the appropriate value handling logic with code.
The isValid will just validate the state on binding time and that's that.
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).
I'm trying to create a one view where I'll could edit data from 2 different model. I read this manual: Multiple models in a view and some other topics on SO, and I successful got data from 2 models in my view, but I can't understand why I can't edit it in my view.
So, in my view I have smth like:
#model Tuple<GroupProjectsModel,InfrastructureModel>
#Html.EditorFor(m => m.Item1.Data)
#Html.EditorFor(m => m.Item2.Data)
Then, when I'm trying to save data, some mistakes happened. I have following code in controller:
[HttpPost]
[InitializeEditPageAttribute]
public ActionResult Edit(GroupProjectsModel get, InfrastructureModel getInf)
{
if (ModelState.IsValid)
{
....
return ReturnView(get.Id, NameModule);
}
var tuple = new Tuple<GroupProjectsModel, InfrastructureModel>(get, getInf);
return View(tuple);
}
In this case variable ModelState.IsValid is equal false anyway. But I'm trying another case:
[HttpPost]
[InitializeEditPageAttribute]
public ActionResult Edit(Tuple<GroupProjectsModel, InfrastructureModel> tupleFromModel)
{
if (ModelState.IsValid)
{
...
return ReturnView(tupleFromModel.Item1.Id, NameModule);
}
var tuple = new Tuple<GroupProjectsModel, InfrastructureModel>(tupleFromModel.Item1, tupleFromModel.Item2);
return View(tuple);
}
and got mistake too. I have no idea what to do.
Just created another class that holds both classes.
public class EditViewModel
{
public GroupProjectsModel groupProjectsModel {get; set;}
public InfrastructureModel infrastructureModel {get; set;}
}
i want to show a dropdownList on a page using Entity Framework in my MVC app, but i am just stuck here to do this using using HTML Helper. so if anyone having knowledge of entity framework, help me...
my dataContext partial class is Entities, in which an entity named MemberInfo have some fields including MemberID & MemberName, so my dropdownList should show the field MemberName & behind this the value should be MemberID,
the code i tried yet--->
#Html.DropDownListFor(Model => Model.MemberID, MemberInfo)
in controller i am returning the Model--->
var MemberNameList = FinanceDBContext.MemberInfoes.Select(x => new { x.MemberID, x.Name });
return View(MemberNameList);
but its not working (errors).
You need to pass in all of your objects as the "model". Best practice is to use a ViewModel which will contain the list of data and a property to store the selected item.
ViewModel
public class MyViewModel
{
// The drop-down list and variable to get selection
public List<Member> Members { get; set; }
public int SelectedMemberId { get; set; }
}
Controller
[HttpGet]
public ActionResult Index()
{
var viewModel = new MyViewModel();
viewModel.Members = FinanceDBContext.MemberInfoes.ToList();
return View(viewModel);
}
[HttpPost]
public ActionResult Index(MyViewModel viewModel)
{
string debug = string.Format("You selected member: {0}", viewModel.SelectedMemberId);
return View(viewModel);
}
Finally, in your view (these lines need to be inside a BeginForm { ... } and ensure your View is strongly typed to MyViewModel
#Html.DropDownList("SelectedMemberId", new SelectList(Model.Members, "MemberID", "Name"))
<input type="submit" value="Save etc..." />
In this example you could put a break-point on the HttpPost action and check the debug string to check the correct Member is returned and proceed as required.
I am getting this error when trying to pass my object to the view. I am new to MVC so please forgive me.
The model item passed into the dictionary is of type 'System.Collections.Generic.List1[<>f__AnonymousType13[System.Int32,System.String,System.Nullable1[System.DateTime]]]', but this dictionary requires a model item of type 'System.Collections.Generic.IEnumerable1[MvcApplication1.Models.storageProperty]'
I am trying to pass a list for a table that will show objects from the storageProperty table with the last date (if there is one) from the expenses table. Most properties have had at least one expense audit, some have had many, and others have had none.
Here is the code from the controller:
var viewModel = db.storageProperties.Select(s => new
{
s.storagePropertyId,
s.BuildName,
latestExpenseSurvey = (DateTime?)s.expenses.Max(e => e.expenseDate)
}).ToList();
return View(viewModel);
}
and the #model statement in the view calls for a storageproperty object. I am using mvc3 with the entity framework. It appears obvious that I cannot pass this list object in place of the storageproperty object, but I can't figure out what to do instead, how should I do this?
Thanks in advance.
Never pass anonymous objects to views. You should always pass view models.
So as always in an ASP.NET MVC application you start by defining a view model which will reflect the requirements of your view:
public class MyViewModel
{
public int StoragePropertyId { get; set; }
public string BuildName { get; set; }
public DateTime? latestExpenseSurvey { get; set; }
}
Then in your controller return an IEnumerable<MyViewModel>:
public ActionResult Index()
{
var viewModel = db.storageProperties.Select(s => new MyViewModel
{
StoragePropertyId = s.storagePropertyId,
BuildName = s.BuildName,
LatestExpenseSurvey = (DateTime?)s.expenses.Max(e => e.expenseDate)
}).ToList();
return View(viewModel);
}
and finally strongly type your view to a collection of this view model:
#model IEnumerable<MyViewModel>
<div>
#Html.EditorForModel()
</div>
Your Linq query projects to an anonymous type. You need to create a named type for this projection in order to refer to it from the view.