MVC - Polymorphic view binding possible? - c#

Can I have a single View in a MVC project that handles multiple derived ViewModel classes? I'm currently using ASP Core RC1 targetting 4.5 .NET framework.
My derived ViewModels have specific validation implemented with data annotations. If I pass a derived model class object to the View that references the base model (#model Models.BaseModel) none of the data annotations are rendered client side with the html 5 data-val tags.
If I use strongly typed views (#model Models.ChildModel) it works as expected. I cannot use more than one #model declaration in a View so I'm unable to check the type of the model in the View and choose the type of model being rendered.
However, I want to use a shared view because there are many fields and only the validation implementation needs to change based on which derived class is being used.
Here's an example implementation:
public abstract class BaseModel
{
[Required]
public abstract string FieldTest {get; set;}
}
public class ChildModel : BaseModel
{
[Email]
public override string FieldTest {get; set;}
}
public class AnotherChildModel : BaseModel
{
[Phone]
public override string FieldTest {get; set;}
}
Here's really what I'm trying to achieve in the View:
#if(Model is ChildModel)
{
#model Models.ChildModel
}
else
{
#model Models.AnotherChildModel
}
At present time my best solution is have a separate View for each derived class view model. The problem with that is the views are merely duplications with different #model references..

At present time my best solution is have a separate View for each
derived class view model. The problem with that is the views are
merely duplications with different #model references..
It seems that underlying problem is you want to eliminate duplicate codes between Views.
If so, you can create Partial View, and share between Views.
For example,
Edit.cshtml
#model UserCreateUpdateModel
#using (Html.BeginForm("Edit", "Users", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.Partial("_CreateOrUpdate", Model)
}
Create.cshtml
#model UserCreateUpdateModel
#using (Html.BeginForm("Create", "Users", FormMethod.Post))
{
#Html.Partial("_CreateOrUpdate", Model)
}
_CreateOrUpdate.cshtml
#model UserCreateUpdateModel
#if (Model.Id > 0)
{
// Keep Edit only fields here, or place them in Edit.cshtml
}
else
{
// Keep Create only fields here, or place them in Create.cshtml
}
// Keep shared fields for both Create and Edit mode
Update
I just notice that you are using same property for different purpose. Please do not do that. It hides the acknowledgement of property - inside any class other than ViewModel class. Maintenance will become nightmare.
It is ok to inherit ViewModel from BaseViewModel (we all do that), but not the way you are overriding it.
I suggest to use separate property - public string Email {get; set;} and public string Phone {get; set;}
public abstract class BaseModel
{
[Required]
public abstract string FieldTest {get; set;}
^^^^^^^
}
public class ChildModel : BaseModel
{
[Email]
public override string FieldTest {get; set;}
^^^^^^^
Store email
}
public class AnotherChildModel : BaseModel
{
[Phone]
public override string FieldTest {get; set;}
^^^^^^^
Store phone number
}

You can use the base type as the children can be referenced that way,
I have previously done this myself using dynamic objects. I am assuming your model at the top is just an example of a model and not really representative of the final product.
#model basetype
#{
dynamic testModel;
if(Model.GetType().Name == typeof(ChildModel).Name)
testModel = new ChildModel();
else if(Model.GetType().Name == typeof(AnotherChildModel).Name)
testModel = new AnotherChildModel();
}
You could also have a flag or enum to indicate which child it is throughout the page so that details may be changed on the page.
I haven't tested the below but you may be able to even use something like the below if there are only two options it could be:
var testModel = Model.getType().Name == typeof(ChildModel).Name ? new ChildModel() : new AnotherChildModel();

You can keep the view to accept the base type, which is BaseModel.
#model Models.BaseModel
In the action method, you can send ChildModel or AnotherChildModel type objects from your action to the view. Since both are derived types of BaseModel , the view should be able to handle either of the derived types. You don't have to really set it the way you are doing in and if else manner. Just setting it to base type should be enough.
OR
You can also make use of templated view helper emthods like EditorFor() which are exactly for these kinds of situations where you want some amount of polymorphism carried over to your views as well.
You can see this link which may help you - Using a single view for derived mvc models

Use Interface instead of concrete type. and everything will should be fine...
What i mean by that is
You can have a model of type interface ,
let's Say IBaseModel
#model IBaseModel
#using (Html.BeginForm("Create", "Users", FormMethod.Post))
{
#Html.Partial("_CreateOrUpdate", IBaseModel)
}
// instead of this than all you need to do is cast to right model
#if(Model is ChildModel)
{
#model Models.ChildModel
}
else
{
#model Models.AnotherChildModel
}
// in this case you will be able use both types and if your base class is implementing it you don't have to do much of refactoring.
IBaseModel as ChildModel.something
IBaseModel as AnotherChildModel.something

The model is set in the controller, so ideally the view shouldn't care which model it gets as long as the model is/inherits from/implements the #model. For handling which view to show depending on which model, the best way I've found is using partial views which are shown based on the name of the model.
Using a non-abstract base model...
#model MyApp.Models.BaseModel
#using (Html.BeginForm...
{
#Html.DisplayFor(model => model.BaseProperty)
#await Html.PartialAsync(String.Concat("_", Model.GetType().Name, "SomePartial"), Model);
}
This requires naming conventions for the partials that follow those of the models. So SquirrelModel would have its unique properties displayed in _SquirrelModelDetailsPartial.cshtml and so forth. This eliminates the need for checking in the view. ChipmunkModel would then trigger _ChipmunkModelDetailsPartial.cshtml and so forth.
I tried the abstract/interface approaches but ran into issues with testing controller actions that post data.

Related

does a partial view always need a model passed from the top-level view?

Here's a url describing partial views in MVC:
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial
Based on this url it looks like partial views are bound to a model that's passed to it from the partial view's top-level/parent view. Is this the standard and expected way to implement partial views?
This seems to indicate that a partial view intended to be used from several different parent views should have some type of associated specialized class that can be used to return its data to multiple different viewmodel builders. Is this the correct interpretation of the MVC partial view architecture?
Yes. By default it uses the parent views (view) model. But you can always pass another model to it explicitly ( as long as the type of the model passing is the same type which the view is strongly typed to).
#Html.Partial("MyPartialView",Model)
Or
#{ var data = new MyClass { SomeProperty = "SomeValue"};
#Html.Partial("MyPartialView",data )
Assuming MyPartialView is strongly typed to MyClass
#model MyClass
For example, If your main view is strongly typed to Order class which has a Customer property like this
public class Order
{
public int OrderId { set;get;}
public Customer Customer { set;get;}
}
public class Customer
{
public string Name { set;get;}
}
You can call the partial view which is strongly typed to the Customer class from your main view by passing the Model.Customer
#model Order
<h2>#Model.OrderId</h2>
#Html.Partial("Customer",Model.Customer)
Assuming your Customer view is strongly typed to Customer type
#model Customer
<h2>#Model.Name</h2>
You can call the Customer partial view from anywhere as long as you have a Customer object to pass to it. ( IF your parent view is strongly typed to Customer class, you do not need to explicitly pass it)

how i can create a shared partial view inside my asp.net mvc web application that can accept different types of objects

I am working on an asp.net MVC web application.and i have many model classes which represents servers, vm, PC, monitors, etc. And for each of these model classes there is a shared class which is used to populate third party API. so i have extended all my model classes using the shared class, as follow :-
public class Server : CreateResource, IValidatableObject
{//code goes here}
public class VM : CreateResource, IValidatableObject
{//code goes here}
public class PC : CreateResource, IValidatableObject
{//code goes here}
and here is the CreateResource class:-
public class CreateResource
{
public CreateResource()
{
this.operation = new Operation5();
this.createAccount = new CreateAccount();
}
public Operation5 operation { get; set; }
public CreateAccount createAccount { get; set; }
}
now the problem i am facing is that for all the model classes , i will be using the exact view for entering the CreateResource data when creating server,vm,pc objects. So at the server,vm,pc, etc main create/edit view i added references to the partial views as follow (this is an example of the Server object):-
#model S.Models.Server
#Html.Partial("_PMCreateResource",Model.operation.Details)
#Html.Partial("_PMCreateAccount",Model.createAccount.operation.Details.ACCOUNTLIST.ToList())
But the problem i am facing is that when the view is posted back to the Create/Edit action method i have to define separate parameters to access the posted back models (one main model and 2 models representing the partial views) as follow (this is an example of the Server action method):-
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Server sj,Details4 d4,List<ACCOUNTLIST> al)
{
and to be able to only define the Server object as follow:-
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Server sj)
{
then i will need to pass the whole model to the partial view as follow (this is an example of the Server main view):-
#model S.Models.Server
#Html.Partial("_PMCreateResource",Model)
#Html.Partial("_PMCreateAccount",Model)
but passing the whole server, vm,pc, monitor models to the same partial view mean that i have to create separate partial views for each model class, since each partial view will be accepting different model object. so not sure if i can modify my code to achieve these 2 things:-
pass the whole model object to the partial view.so when posting back the view the post action method will only accept one model object as the parameter, instead of 3 parameters.
use a single shared view, which will accept different types of model objects?
not sure how i can achieve this?
Do not use #HtmlPartial(), use an EditorTemplate so that the name attributes of the controls are correctly prefixed.
Create an partial view named CreateResource.cshtml in the /Views/Shared/EditorTemplates folder (note that the name of the file must match the name of the class)
#model CreateResource
#Html.TextboxFor(m => m.operation.Details.SomeProperty)
....
for(int i = 0; i < Model.createAccount.operation.Details.ACCOUNTLIST.Count; i++)
{
#Html.TextBoxFor(m => m.createAccount.operation.Details.ACCOUNTLIST[i].SomeProperty)
....
}
Then in the main view
#model S.Models.Server
Html.EditorFor(m => m) // generated the base controls
.... // controls specific to Server
Then to break the EditorTemplate into more manageable parts and allow you to reuse them, create additional templates for Operation5 and CreateAccount
In /Views/Shared/EditorTemplates/Operation5.cshtml
#model Operation5
#Html.TextBoxFor(m => m.Details.SomeProperty)
....
and change the CreateResource.cshtml template to
#model CreateResource
#Html.EditorFor(m => m.operation)
#Html.EditorFor(m => m.createAccount)
and you can keep breaking this down to create an EditorTemplate for each nested model, including for the collection items, so assuming property Details is typeof Detail and property ACCOUNTLIST is typeof List<AccountItem>, then you would have
/Views/Shared/EditorTemplates/AccountItem.cshtml
#model AccountItem
#Html.TextBoxFor(m => m.SomeProperty)
/Views/Shared/EditorTemplates/Detail.cshtml
#model Detail
#Html.TextBoxFor(m => m.SomeProperty)
#Html.EditorFor(m => m.ACCOUNTLIST) // generates the html for each item in the collection

How to send data from nested ViewModel to Controller

I am using nested view models to display views based on user roles.
Model:
public class MainVM {
//some properties
public OneVM One {get; set;}
public TwoVM Two {get; set;}
}
public class OneVM {
//properties
}
public class TwoVM {
//properties
}
As written here that only main model is need to be sent controller. I am using Automapper to map properties from received model.
Controller:
public ActionResult EditAction(MainVM model){
var item = db.Table.Find(model.Id);
//automapper to map
AutoMapper.Mapper.Map(model.One, item); //does not work
db.Entry(item).State = EntityState.Modified;
db.SaveChanges();
}
Is this the right way to do that? What am I doing wrong here.
Update:
This was the view I was using to render nested view models from partial views
View:
#model MainVM
#Html.RenderPartial("_OnePartial", Model.One)
This answer https://stackoverflow.com/a/6292180/342095 defines an Html helper which will generate the partial view with right names.
The value of property One will be empty because you are passing an instance of OneVM to the partial (not the main model) so the form controls are not correctly named with the prefix (which need to be name="One.SomeProperty").
You have included a link to a PartialFor() helper (which works) but don't use it. In the main view it needs to be
#Html.PartialFor(m => m.One, "_OnePartial")
Which is the equivalent of
#Html.Partial("_OnePartial", Model.One,
new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "One" }})
The problem probably lies in your HTML. If a model is nested, then the input fields of properties should be like this:
<input type="text" name="SubModel.PropertyName" />
Using HTML helpers, it would look something like this:
#Html.EditorFor(model => model.SubModel.PropertyName)
The ASP.NET MVC Action cannot know, that you want to fill your submodel if it's not in your HTML.

Passing a model property to a partial view

I'm having trouble passing the correct data to a partial view, and I'm not sure why it is failing.
Say I have models
public class SubModel {
public string Wobble {get; set;}
}
public class MyModel {
public SubModel Wibble {get; set;}
}
and views
MyView.cshtml
#model MyModel
#Html.Partial("SomePartial", Model.Wibble)
and
SomePartial.cshtml
#model SubModel
<h1>Victory!</h1>
this fails with The model item passed into the dictionary is of type 'MyModel', but this dictionary requires a model item of type 'MySubModel'
When I change MyView.cshtml to
#model MyModel
#Html.Partial("SomePartial", Model.Wibble, new ViewDataDictionary<MySubModel>(Model.Wibble))
It works as expected.
Why do I need to explicitly pass a ViewDataDictionary? Why can't I just pass the model?
Note: I'm using a library that does things that make me go hrm? a lot of the time. If the above behaviour is not expected, it might be this libraries fault.
This happens when the model you pass to the partial view is null. Don't pass null to a partial view, or it will get confused about its type.
Did you try passing the submodel with Model.Wibble instead of MyModel.Wibble? The associated model is accessible in a view with Model, not the model's name.

Supplying a ID to many partial views from a parent view without exposing sensitive ID's

We are using a friendly Name URL route scheme. Basically using a combination of the Principal Identity and a friendly name this can be mapped back to an identity internally (Person ID). So a route like this:
routes.MapRoute(string.Empty, "{friendlyname}/Products/{action}", new {controller = "Products", action = "List"});
Would map to a URL like this:
Adam/Products/List
This all works fine and abstracts away the internal Id of the named person which is required as well.
The problem is our Views are comprised of many partial views. When there are rendered by using the #Html.Action method they ultimately need the PersonID but from the URL we only have the 'friendly name'.
I have thought about this for a while and there are two solutions to my mind:
Pass the 'friendly name' into each of the controller action methods that return the partial views and internally the method will have to do a lookup on the currently logged in identity and the friendly name. This will give the PersonID to me and I can then efficiently query from then on. The only problem with this apporach is that due to the multiple partial views I will be querying on the currently logged in identity and friendly name for each partial view call which is innefficeint and I feel I should only have to write this code once.
Somehow query in the view and get the PersonID so it can be passed to each #Html.Action call so the partial view controller methods will not have to do that lookup themselves saving round trips to the database for the same shared informtion. The problem with this is that I am not sure of a way of doing this cleanley in the view using the DI that we use through the rest of the application.
Any thoughts on approach to would be greatly appreciated.
Thank you,
Adam
You could add the Id to the session variables and access it from within the views with:
#{var personId = int.Parse(Session["PersonId"])}
Then you can pass it directly to partial views from the Parent without it hitting the client or having to pass parameters to any controllers.
Update
You could also access the session variable from the Controller if you wanted to do the work there instead without roundtripping to the database.
EDIT
If you put the property in a model and pass it to a page that post's back then the model will not persist between posts.
If for example your controller does:
[HttpPost]
public ActionResult DoSomething(ViewModel model)
{
if(ModelState.IsValid)
{
// Logic Here
}
return View(model)
}
when the page is reloaded, the model will have forgotten about the ID.
There are a couple of ways around this. Either use #Html.HiddenFor(m => m.ID)
which will put the property in the rendered HTML, which if it is truely a sensitive piece of information, is bad.
Or you can rebuild the view model on each subsequent postback.
Hope this helps
As Marc states I could you the Session to deal with this but I have gone with using the Model as he has stated in his update. If the parent View Controller action takes in the friendly name it can do the lookup, put the PersonID into the model and then any Partial Renders can have the models value passed into them in the parent View Controllers Action's view. An example is shown below (this is demo code but it hopefully gets the point across, I would never use a static data context in real code)
Home controller
public class HomeController : Controller
{
public ActionResult Index(string friendlyName)
{
int pupilId = Data.People.Single(x => x.Name == friendlyName).PersonId;
HomeIndexViewModel homeIndexViewModel = new HomeIndexViewModel {PupilId = pupilId};
return View(homeIndexViewModel);
}
}
Home Index View
#model SharingInformationBetweenPartials.Web.Models.HomeIndexViewModel
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#Html.Action("DisplayPersonDetail", "Person", new {Model.PersonId})
The PersonController's DisplayPersonDetail method can then present the respective data it wishes using the passed in PersonId:
public class PupilController : Controller
{
[ChildActionOnly]
public ActionResult DisplayPupilDetail(int pupilId)
{
Person person = Data.People.Single(x => x.PersonId == pupilId);
return View(person);
}
}
I did try what I thought was this before but I must have got something wrong as the ViewModels properties were getting shown in the URL which is what I was trying to get away from. Anyway, I hope this helps anyone else who may be looking to do something similar. If you have any questions then let me know.
Thanks,
Adam
You can use [Bind] attribute to specify the exact properties a model binder should include in binding or use Exclude parameter on the attribute to exclude PersonId
[HttpPost]
public ActionResult EditPerson([Bind(Exclude = "PersonId")] Person person)
{
//do domething
}
You can also use [ReadOnly] attribute that model binder will understand and not assign to that property.
[ReadOnly(true)]
public int PersonId{ get; set; }
But the best approach is to use separate ViewModels: one only for viewing and one for editing.
public abstract class Person
{
public string Name { get; set; }
public string Surname { get; set; }
}
public class PersonCreateVM : Person
{
//no PersonId here
}
public class PersonEditVM : Person
{
public int PersonId{ get; set; }
}
This approach is maybe a "overkill" but when used properly and with AutoMapper http://automapper.codeplex.com/ it's an ease to work with.

Categories

Resources