3

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..

clovola
  • 378
  • 1
  • 14
  • 2
    Short answer is no. But the fact you have a `[Email]` validation on one and a `[Phone]` validation on the other makes no sense - they need to be separate properties, not overrides –  Feb 02 '17 at 22:23
  • I'm curious about this, if the answer is no then I basically saved myself repeating code in two separate ViewModels only to wind up duplicating Views. I understand the issue with the data annotations, @win pointed that out too. That's just an example demonstrating my need for different annotations in each child. In reality I'm validating date values differently depending on what derived type it is. – clovola Feb 02 '17 at 23:06
  • 2
    In that case, creating a custom validation attribute using conditional validation based on a value in your view model may solve the issue (but without seeing your real code its impossible to tell) –  Feb 02 '17 at 23:10

5 Answers5

4

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,

enter image description here

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
}
Win
  • 61,100
  • 13
  • 102
  • 181
  • I can try this but I'm wondering what the PartialView's @model reference would be, or better put, of which model type? If it's the BaseClass I'll end up in the same situation. In my situation all of the fields are the same but the validation applied to each is different. So when the view is being rendered (or before perhaps?) it needs to know which DerivedClass type the model that's being rendered belongs to in order to apply the validation rules. – clovola Feb 02 '17 at 21:57
  • 1
    I updated my answer. Please do not do that. It is not a good practice of inheritance. – Win Feb 02 '17 at 22:19
  • I see what you are saying. That was just an example class setup. I'm not using Email/Phone validators in my actual code. I'm actually using custom annotations that are doing date validation. This was just demonstrating the need for different annotation types. – clovola Feb 02 '17 at 23:03
  • 2
    Well, I think that it might be a good solution assuming that the base model is big...  You would simply display the appropriate partial view with specific model for parts that differ. However, it might be simpler to just add extra property in the base model and hide (don't generate HTML) for part that are unused. Alternatively, another option is to reverse essentially the whole thing. You have a partial view for the common view and each specific view only add its specific data. It could work relatively well if only one section of the page is affected. – Phil1970 Feb 03 '17 at 00:46
0

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  
  • An interface instead of an abstract class? I would have to then implement all of the fields in each derived ViewModel correct which would cause a lot of duplication in my ViewModels. If I understand you correctly. – clovola Feb 02 '17 at 21:54
  • I just edited my answer so you get it better understanding what i mean have a look hope this helps – Arman Nagaepetian Feb 03 '17 at 20:32
  • Thank you for the update. I get a runtime exception when I do this and attempt to access a view -"Only one 'model' statement is allowed in a file.". So the model IBaseModel, model Models.ChildModel, and Models.AnotherChildModel total 3 total 'model' statements. However I have not attempted to use the model statement with an interface only abstract/derived classes. – clovola Feb 03 '17 at 22:29
  • You have to remove this if else thing if you will work with interface but i guess you have never programed against interfaces before ? it will be nice point to start look at some tutorials programming against interfaces. – Arman Nagaepetian Feb 04 '17 at 09:50
0

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();
Slipoch
  • 750
  • 10
  • 23
0

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

Community
  • 1
  • 1
Arathy T N
  • 306
  • 3
  • 13
  • Initially, I had tried your first approach. The view could accept either of the derived model types by using @model Models.BaseModel. However, because the view didn't know which of the derived types to use it didn't load the associated validation attributes to the controls only the annotations from the base class. I attempted at another point to use the EditorFor() which I may need to revisit. When I tried this it loaded my entire ViewModel into markup but I don't necessarily want to expose all of the fields. There's probably a way to customize this I will examine this further, thanks. – clovola Feb 03 '17 at 05:08
  • What happens if you dont add any validation attribute to the base model's property? Instead, you add that only to the derived types. Does that help picking up the child type's attributes? – Arathy T N Feb 03 '17 at 06:47
  • That was worth a shot. I can see why that might work because of the overriding in the child classes, I attempted this however and it didn't work. Along the same lines of thinking I attempted to use a virtual field instead of an abstract field but no dice either. – clovola Feb 03 '17 at 22:34
  • Have you tried using a partial metadata class for defining the data annotations for the derived classes? Pls see how to add it - https://www.asp.net/mvc/overview/getting-started/database-first-development/enhancing-data-validation. Similar question - http://stackoverflow.com/questions/6550409/how-to-add-attributes-to-a-base-classs-properties – Arathy T N Feb 04 '17 at 18:23
0

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.

tnJed
  • 838
  • 9
  • 12