MVC4 - Partial View Model binding during Submit - asp.net-mvc-4

I have view model which has another child model to render the partial view (below).
public class ExamResultsFormViewModel
{
public PreliminaryInformationViewModel PreliminaryInformation { get; set; }
public string MemberID { get; set; }
public string MemberName { get; set; }
public int PatientID { get; set; }
public string ConfirmationID { get; set; }
public bool IsEditable { get; set; }
#region Select Lists
public SelectList ProviderOptions { get; set; }
#endregion
}
public class PreliminaryInformationViewModel
{
public string ProviderName { get; set; }
public string ProviderID { get; set; }
public string ServiceLocation { get; set; }
}
This PreliminaryInformationViewModel view model also used as a child models in another view model since this preliminary information can be updated at different pages.
So I created this preliminary information as a separate partial and to include in other pages.
#{Html.RenderPartial("_PreliminaryInformation", Model.PreliminaryInformation);}
Inside the partial
#model Web.Models.Preliminary.PreliminaryInformationViewModel
<div>
#Html.TextBoxFor(x => x.DateOfService })
</div>
But the problem is during submit this preliminary model is always null due to the reason HTML name attribute is always is rendered as
but when I pass the parent model to the partial as below.
#model Web.Models.Exam.ExamResultsFormViewModel
<div>
#Html.TextBoxFor(x => x.PreliminaryInformation.DateOfService })
</div>
Now the HTML element is generated as
<input type = 'text' name='PreliminaryInformation.DateOfService.DateOfService' id='PreliminaryInformation.DateOfService'>
and it binds properly during the submit.
I understand MVC bind the element value based on the name attribute value, but the second implementation would need me to create a multiple partial for each page, which I don't like.
So far I couldn't find a solution to work with the first implementation, is there way I can make preliminary information model value bind during submit with the first implementation.

I know its a bit late but it might help to someone
If you have complex model, you can still pass it into partial using:
#Html.Partial("_YourPartialName", Model.Contact, new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "Contact"
}
})
where I have defined model with property "Contact". Now what HtmlFieldPrefix do is add the property binding for each model "so the model binder can find the parent model"
There is a blog post about it: http://www.cpodesign.com/blog/bind-partial-view-model-binding-during-submit/
.NET Core 2 binding
In .NET Core 2 and MVC the answer above will not work, the property is no longer settable.
How ever the solution is very similar.
#{ Html.ViewData.TemplateInfo.HtmlFieldPrefix = "Contact"; }
#await Html.PartialAsync("_YourPartialName", Model.Contact)
after you can submit your model, it will bind again.
Hope that helps

You can add the HtmlFieldPrefix to the top of your partial view:
#{
ViewData.TemplateInfo.HtmlFieldPrefix = "Contact";
}
This is the same approach as that described by #cpoDesign but it means you can keep the prefix in your partial view if you need to do that.

You need to create an editor template for PreliminaryInformationViewModel to replace the partial view, then call with Html.EditorFor( m => m.PreliminaryInformation ). Reference this solution. Creating the template should be as simple as moving your partial view to the Views/Shared/EditorTemplates directory. Html.EditorFor(...) will automatically use this template based on the type you're passing in as the model (in this case, PreliminaryInformationViewModel)

I also ran into this problem. I will explain my solution using your code. I started with this:
#{
Html.RenderPartial("_PreliminaryInformation", Model.PreliminaryInformation);
}
The action corresponding to the http post was looking for the parent model. The http post was submitting the form correctly but there was no reference in the child partial to the parent partial. The submitted values from the child partial were ignored and the corresponding child property remained null.
I created an interface, IPreliminaryInfoCapable, which contained a definition for the child type, like so:
public interface IPreliminaryInfoCapable
{
PreliminaryInformationViewModel PreliminaryInformation { get; set; }
}
I made my parent model implement this interface. My partial view then uses the interface at the top as the model:
#model IPreliminaryInfoCapable
Finally, my parent view can use the following code to pass itself to the child partial:
#{
Html.RenderPartial("ChildPartial", Model);
}
Then the child partial can use the child object, like so:
#model IPreliminaryInfoCapable
...
#Html.LabelFor(m => m.PreliminaryInformation.ProviderName)
etc.
All of this properly fills the parent model upon http post to the corresponding action.

Quick Tip: when calling your EditorFor method, you can set the name of the template as a parameter of the Html.EditorFor method. Alternatively, naming conventions can be your friend; just make sure your editor template filename is exactly the same name as the model property type
i.e. model property type 'CustomerViewModel' => 'CustomerViewModel.cshtml' editor template.

Please make the below changes to your partial page. so it will come with your Parent model
//Parent Page
#{Html.RenderPartial("_PreliminaryInformation", Model.PreliminaryInformation);}
//Partial Page
#model Web.Models.Preliminary.PreliminaryInformationViewModel
#using (Html.BeginCollectionItem("PreliminaryInformation", item.RowId, true))
{
<div>
#Html.TextBoxFor(x => x.DateOfService })
</div>
}

For .net core 2 and mvc, use do like below:
#{
Html.ViewData.TemplateInfo.HtmlFieldPrefix = "Contact";
}
#await Html.PartialAsync("_YourPartialViewName", Model.Contact)

Related

Unobtrusive validation doesn't work with ViewComponents

I'm implementing a form with ASP.NET Core v3.1.
I have code for a drop-down on a Razor page which looks like this:
<div class="form-group">
<label asp-for="MySelectedItem" class="m-b-none"></label>
<help-text asp-for="MySelectedItem"></help-text>
<div class="input-group">
<select asp-for="MySelectedItem" asp-items="#Model.MyItems" class="form-control"></select>
<partial name="_ValidationIcon" />
</div>
<span asp-validation-for="MySelectedItem" class="validation-message"></span>
</div>
In my model class, I've included a validation rule to ensure that a valid option must be selected. This renders nicely:
Now, I want to genericise this as a View Component so that I don't have to paste this lump of code anytime I want a drop-down. Here is my implementation:
public class DropdownViewComponent : ViewComponent
{
public IEnumerable<SelectListItem> Items { get; set; }
public ModelExpression SelectedItem { get; set; }
public async Task<IViewComponentResult> InvokeAsync(ModelExpression selectedItem, IEnumerable<SelectListItem> items)
{
Items = items;
SelectedItem = selectedItem;
return View(this);
}
}
/Dropdown/Default.cshtml
#model Web.ViewComponents.DropdownViewComponent
<div class="form-group">
<label asp-for="SelectedItem" class="m-b-none"></label>
<help-text asp-for="SelectedItem"></help-text>
<div class="input-group">
<select asp-for="SelectedItem" asp-items="#Model.Items" class="form-control"></select>
<partial name="_ValidationIcon" />
</div>
<span asp-validation-for="SelectedItem" class="validation-message"></span>
</div>
Usage:
<vc:dropdown selected-item="MySelectedItem" items="Model.MyItems"></vc:dropdown>
This code correctly renders the drop-down, but the validation attributes are missing from the rendered HTML. Why?
I'm also not sure how to get the DisplayName from the model expression that I passed to the View Component.
Where have I gone wrong?
Thanks.
Presumably, your validation attributes are on the MyItems property of your parent model, which isn't listed here. E.g.,
public class ParentModel {
[Required]
public IEnumerable<SelectListItem> MyItems { get; set; }
}
As such, the issue is that your view is no longer binding to that model or its MyItems property. Instead, it's binding to the Items property on the DropdownViewComponent model, which doesn't have any validation attributes on it. Those two properties may both be pointing to the same IEnumerable<SelectListItem> object reference, but their metadata is entirely different. As such, ASP.NET Core is correctly displaying the validation attributes associated with the DropdownViewComponent.Items property.
I see this as an unfortunate limitation of view components—but one that's conceptually difficult to reason out of. For more information, see my answer to a similar question I previously posed, How to bind a ModelExpression to a ViewComponent in ASP.NET Core.
That said, given your particular requirements, you can work around this by passing the model which contains the target property to your view component—instead of passing the value of the target property itself—and then relaying that via your view component's view model:
public class DropdownViewComponent : ViewComponent
{
public ParentModel ParentModel { get; set; }
public ModelExpression SelectedItem { get; set; }
public async Task<IViewComponentResult> InvokeAsync(ModelExpression selectedItem, ParentModel model)
{
ParentModel = model;
SelectedItem = selectedItem;
return View(this);
}
}
Note: I would typically create a separate, lightweight view model for your view component, instead of passing your view component object down to its view. But I'm maintaining this structure for consistency with your original code.
You would then be able to bind to the original property in your view component's view, thus maintaining all of the original validation attributes:
<select asp-for="SelectedItem" asp-items="#Model.ParentModel.MyItems"></select>
On first approximation, this doesn't buy you much—and may even defeat your entire reason for pursuing a view component in the first place—as it forces you to operate against a single model. If multiple views with different models want to use this view component, that introduces some problems. You can mitigate these, however, by introducing a layer of abstraction.
There are a few approaches to this, such as establishing an interface, but the one I recommend is to develop a specialized list class which you use to model IEnumerable<SelectListItem>:
public class DropdownList: List<SelectListItem> {
public virtual IEnumerable<SelectListItem> Items { get; } = this;
}
And then working off of that in your DropdownViewComponent:
public class DropdownViewComponent : ViewComponent
{
public DropdownList DropdownList { get; set; }
public ModelExpression SelectedItem { get; set; }
public async Task<IViewComponentResult> InvokeAsync(ModelExpression selectedItem, DropdownList dropdownList)
{
DropdownList = dropdownList;
SelectedItem = selectedItem;
return View(this);
}
}
And, finally, implementing it as follows in your view component's view:
<select asp-for="SelectedItem" asp-items="#Model.DropdownList.Items"></select>
This would allow you to override this class in order to add attributes as needed. E.g.,
public class RequiredDropdownList : DropdownList {
[Required]
public override Items { get; } = this;
}
Note: if you have a need to use a variety of different collections on different view models, and were relying on IEnumerable<SelectListItem> to unify them, this approach won't work. In that case, creating something like an IDropdownList interface makes more sense. Regardless, the concept is virtually identical.

How to Use Two Same Model in a View? [MVC4]

I'm trying to create a status update page where I want a user to insert status message in Index page and also, I want to show all inserted all status messages in the same Index page.
This is my Model code:
public class Statuses
{
[Key]
public int StatusID { get; set; }
[DataType(DataType.MultilineText)]
[Required]
public string message { get; set; }
}
public class StatusContext : DbContext
{
public DbSet<Statuses> Status { get; set; }
}
And, I used #Html.EditorFor(model => model.message) in the Index.cshtml page.
To show the editor, I used the following model in View.
#model LearnStart.Models.Statuses
However, to show all the status messages below the Multiline TextArea, I think I'm supposed to use the below one.
#model IEnumerable<LearnStart.Models.Statuses>
How to use both model in same view so that I can display both the text area (to insert the status message) and to list all available status messages below it?
First, you should not be passing your entities directly to your view. The recommended best practice is to use View Models, which are models tailored specifically to your view.
Second, when using a view model you can now do this, since it's not tied to your data model entities:
public class MyActionViewModel {
public List<StatusesViewModel> StatusList {get;set;}
public StatusesViewModel CreatedStatus {get;set}
}
Then in your view:
#model MyActionViewModel
#Html.EditorFor(x => x.CreatedStatus)
.............................................
#Html.DisplayFor(x => x.StatusList)
Then you can create two templates, an EditorTemplate and a DisplayTempate:
In ~/Views/Shared/EditorTemplates/StatusesViewModel.cshtml
#model StatusesViewModel
#using(Html.BeginForm()) {
#Html.EditorFor(x => x.Message)
<input type="submit" value="Create Status" />
}
In ~/Views/Shared/DisplayTemplates/StatusesViewModel.cshtml
#model StatusesViewModel
<div>
<span>#Model.Message</span>
</div>
The thing that's nice about using the templates is that they will automatically iterate over your collection.. no foreach or for statement is used. A single EditorFor works on the entire collection, then renders the template based on the type, which in this case translates to StatusViewModel.cshtml
Easy way is to put a list inside Viewbag and show list in View as shown :-
Controller :
Public Actionresult Myaction()
{
.........
Viewbag.data = //bind list of messages here
return View();
}
View :
#model LearnStart.Models.Statuses
.........
.........
.........
#if(Viewbag.data != null){
<table>
#foreach(var item in Viewbag.data)
{
<tr><td>#item.message</td></tr>
}
</table>
}

MVC view with two different models

I have a view called AccountManager on this view I have a section to update your profile information and update your password.
Both functions require a different model. ManageUserViewModel and ChangePasswordViewModel.
On the AccountManager view, both sections are rendered via #Html.Partial.
When I try to display the page, I receive the following error: "The model item passed into the dictionary is of type 'WebUI.Models.ManageUserViewModel', but this dictionary requires a model item of type 'WebUI.Models.ChangePasswordViewModel'."
How can I render both views without receiving this error?
ViewModel:
public class AccountManagerViewModel
{
public ManageUserViewModel manageUserViewModel { get; set; }
public ChangePasswordViewModel changePasswordViewModel { get; set; }
}
The View:
#model AccountManagerViewModel
/*HTML CODE*/
#Html.RenderPartial("_PartialChangePassword", Model.changePasswordViewModel)
#Html.RenderPartial("_PartialManageUser", Model.manageUserViewModel)
So you just play with the viewModels. Remember that they can have anything you need in the view. Let me know.

Why does ASP.NET MVC assumes that view will have matching input and output types?

ASP.NET MVC (or rather Html.Helpers and base page implementation) assumes that there will be one type for both rendering and posting (namely Model).
This is a violation of ISP, isn't it?
I am tempted to derive my Edit views (those that have different render-data, and post-data) from a custom EditPageBaseView<TViewModel, TFormData>.
The problem is I want my validation and post work against FormData instance (stored inside ViewModel), but MVC assumes that entire ViewModel will be POSTed back.
Is there an OOB way to facilitate that? (I didn't find one if there is).
Is it a bad idea (in concept) to have separate data types for different operations exposed by a service (a view in this case).
I tend to follow the CQRS model when constructing my view models. All rendering is done with ViewModel classes and all posting back is done with Command classes. Here's a contrived example. Let's say we have a View with a small form for creating users.
The ViewModel and Command classes looks like this:
public abstract class ViewModel {}
public abstract class Command: ViewModel
public class CreateUserViewModel : ViewModel
{
public string Username { get; set; }
public string Password { get; set; }
public string PasswordConfirm { get; set; }
}
public class CreateUserCommand : Command
{
public string Username { get; set; }
public string Password { get; set; }
public string PasswordConfirm { get; set; }
}
The UserController creates a CreateUserViewModel as the model for the Get request and expects a CreateUserCommand for the Post request:
public ActionResult CreateUser()
{
// this should be created by a factory of some sort that is injected in
var model = new CreateUserViewModel();
return View(model);
}
[HttpPost]
public ActionResult CreateUser(CreateUserCommand command)
{
// validate and then save the user, create new CreateUserViewModel and re-display the view if validation fails
}
Model binding takes care of ensuring that the properties of the Posted CreateUserCommand are populated properly, even though the Get View is bound to a CreateUserViewModel.
They don't have to match, but they do match by default.
If you don't want them to match, you can specify a different model in your Form or ActionLink:
Example of a Mismatch using Razor and C#:
Index.chtml:
#model FirstModel
<div>
#using (Html.BeginForm("Action", "ControllerName", new { ParameterName = new SecondModel { First = "First", Second = "Second" } }, FormMethod.Post)) {
<input type="submit" value="Submit Button" />
}
</div>
The Controller:
public class ControllerName : Controller {
public ActionResult Index() {
return View(new FirstModel());
}
public ActionResult Action(SecondModel ParameterName) {
return View() // Where to now?
}

Can I get a SelectList in my view model to render using EditorForModel?

I would like have a listbox of states render using the EditorForModel Html helper.
My view model:
public class MyViewModel
{
public MyewModel()
{
States = new SelectList(MyModel.RegionsToSelectList,"Value","Text");
}
[DataType(DataType.Text)]
public string City { get; set; }
[Display(Name = "States")]
public SelectList States { get; private set; }
}
In my view I have #Html.EditorForModel()
The City renders properly but the States do not render into any sort of list (dropdown or listbox)
If I use #Html.DropDownList("mylistname", Model.States) it renders properly.
I would really like to have it render in the ForModel process.
Can this be done?
You need to use the Html.DropdownListFor helper if you want to generate a drop down list. The fact that you have used SelectList as type to some of your properties doesn't mean that the default editor template will render a box. So you will have to write a custom editor template.
You may take a look at the following blog post to see how those default templates are implemented.