Polymorphic ViewModel collection and rendering in MVC partial Views - c#

I'm having a problem with a polymorphic collection of ViewModels in my MVC application. I received this via a web service call and i need to iterate through them and give them their own partial view, based on the object type.
public abstract class ProvinceViewModel
{
public string Code { get; set; }
}
public sealed class OntarioViewModel : ProvinceViewModel { }
public sealed class QuebecViewModel : ProvinceViewModel {}
In my view i am trying to iterate through them and assign a partial view. I have to do a lot of type casting here to make it work. If I try and move this to a controller action and pass in the abstract type, i will get an error that we cannot create an instance of abstract class.
ICollection<ProvinceViewModel> ProvinceList; // collection receive via service
#for (int i = 0, c = ProvinceList.Count; i < c; i++)
{
var currentProvince = this.Model.ElementAt(i);
#switch (additionalRegistry.Code)
{
case "QC":
#Html.Partial("AlbertaDetail", (QuebecViewModel)currentProvince)
break;
case "ON":
#Html.Partial("OntarioDetail", (OntarioViewModel)currentProvince)
break;
default:
#Html.Partial("ProvinceDetail", ProvinceViewModel)
break;
}
}
I have strongly type View, so that i can access the different properties.
How would i go about solving this in a more elegant way? Would I need to create a new surrogate base class for the abstract class to create a instance of it easier?

You can achieve this with display templates. Create a display template for each type in the DisplayTemplates folder within your Controller's Views directory:
+-- Views
+-- Provinces
+-- DisplayTemplates
+-- OntarioViewModel.cshtml
+-- QuebecViewModel.cshtml
Display each model using the DisplayFor helper in your view:
#model ICollection<ProvinceViewModel>
#foreach (var province in Model)
{
#Html.DisplayFor(_ => province)
}

Upon encountering the same problem in the past, I have created the following solution:
First, decorate your (concrete) view-model with ExportMetadata attribute that denotes the view name to be used. For example:
[ExportMetadata("View", "Ontario")]
public sealed class OntarioViewModel : ProvinceViewModel { }
[ExportMetadata("View", "Quebec")]
public sealed class QuebecViewModel : ProvinceViewModel {}
Then extend your HtmlHelper with the following Partial method:
public static MvcHtmlString Partial<T>(this HtmlHelper htmlHelper, T model, string prefix = null)
{
var modelType = typeof (T);
var partialAttr = modelType.GetCustomAttributes<ExportMetadataAttribute>().SingleOrDefault(x => x.Name == "View");
if (partialAttr == null)
throw new Exception(modelType.Name + " doesn't define any view to be used");
var partialName = (prefix ?? String.Empty) + partialAttr.Value;
return htmlHelper.Partial(partialName, model, htmlHelper.ViewData);
}
Then use it:
#Html.Partial(currentProvince);
And in case your partials reside in some sub-directory:
#Html.Partial(currentProvince, "Partials/")
(If you need help registering the custom HTML helper see https://stackoverflow.com/a/5052790)

I had a similar requirement and this is how I managed to solve this issue.
My viewmodel (BusinessEventEmailViewModel ) has a list of interfaces (IBusinessEventEmail) resolved at runtime with unity. A IBusinessEventEmail has an EventCode property.
public class BusinessEventEmailViewModel : MailHeaderViewModel
{
#region members
public List<IBusinessEventEmail> Events { get; set; }
In my view, I render the partial view using a naming convention :
Html.RenderPartial("~/Views/Shared/Email/_" + businessEvent.EventCode + ".cshtml", businessEvent);
Then, I have a XXXEventEmail implementing IBusinessEventEmail with the EventCode XXX and a partial view _XXX.cshtml

Related

Set/Change Display(Name="") Attribute of a Property in Controller/ViewComponent

According to this answer from 2010, regarding mvc-2, it wasn't possible. What about now, in asp.net-core 2.2?
My usecase:
I have a BaseViewModel that is being used by 2 views: TableView (for users) and TableManagentView (for admins). The BaseViewModel is invoked by a ViewComponent. Here are some samples:
BaseViewModel:
public class BaseViewModel {
[Display(Name = "Comment")
public string UserComment { get; set; }
}
TableView:
#await Component.InvokeAsync(nameof(Base), new { myObject = myObject, stringName = "User"})
TableManagementView:
#await Component.InvokeAsync(nameof(Base), new { myObject = myObject, stringName = "Admin"})
Base:
public class Base : ViewComponent
{
public IViewComponentResult Invoke(BaseViewModel myObjet, string invokingView)
{
// here i want to do something like that
if (invokingView == "User") {
myObject.UserComment.SetDisplayName("My Comment");
}
if (invokingView == "Admin") {
myObject.UserComment.SetDisplayName("User's Comment");
}
return View("BaseViewComponent", myObject);
}
}
BaseViewComponent:
#Html.DisplayNameFor(model => model.UserComment)
The BaseViewModel is simplified, but there are a lot more attributes. The reason I want to do this is to avoid code duplication in both tables. The only thing that should change are the label names.
I've tried reflection, but without success:
public IViewComponentResult Invoke(BaseViewModel myObject, string invokingView)
{
MemberInfo property = typeof(BaseViewModel).GetProperty("UserComment");
property.GetCustomAttributes(typeof(DisplayAttribute)).Cast<DisplayAttribute>().Single().Name = "test";
return View("BaseViewComponent", myObject);
}
The Name doesn't change and remains "Comment" from the initial setting.
If it's not possible to set the attribute name programmatically, what other solutions do I have? I'm thinking about ViewBag/ViewData or TempData, but this solution doesn't appeal to me. What would be the pro's and con's of that?
Extending on the comment I left, one way you could solve this is by having your BaseViewModel being an abstract class and have concrete classes deriving from it. So UserViewModel and AdminViewModel. These two concrete classes would then be the models for both TableView and TableManagentView and would be responsible for telling the "outside world" how to label fields.
The base class has two main aspects (apart from your normal fields): An abstract Dictionary<string, string> which will contain the labels and a method to get the label from the list: string GetLabel(string propName). So something like this:
public abstract class BaseViewModel
{
protected abstract Dictionary<string, string> Labels { get; }
public string UserComment { get; set; }
public string GetLabel(string propName)
{
if (!Labels.TryGetValue(propName, out var label))
throw new KeyNotFoundException($"Label not found for property name: {propName}");
return label;
}
}
Then you create the two deriving classes User and Admin:
public sealed class UserViewModel : BaseViewModel
{
protected override Dictionary<string, string> Labels => new Dictionary<string, string>
{
{ nameof(UserComment), "User label" }
};
}
public sealed class AdminViewModel : BaseViewModel
{
protected override Dictionary<string, string> Labels => new Dictionary<string, string>
{
{ nameof(UserComment), "Admin label" }
};
}
They only implement the Dictionary<string, string> and set the appropriate text for each field on the base class.
Next, changing your BaseViewComponent to this:
View:
#model DisplayNameTest.Models.BaseViewModel
<h3>Hello from my View Component</h3>
<!-- Gets the label via the method on the base class -->
<p>#Model.GetLabel(nameof(BaseViewModel.UserComment))</p>
<p>#Model.UserComment)</p>
ComponentView class (simpler now)
public IViewComponentResult Invoke(BaseViewModel viewModel)
{
return View(viewModel);
}
Finally, changing your views TableView and TableManagentView to this:
#model WebApp.Models.AdminViewModel
#{
Layout = null;
}
<h1>Admin View</h1>
<div>
#await Component.InvokeAsync("Base", Model)
</div>
and the Controller to:
public IActionResult Index()
{
var adminViewModel = new AdminViewModel { UserComment = "some comment from admin" };
return View(adminViewModel);
}
Now when you navigate to TableView, you'll pass a UserViewModel to the BaseViewComponent and it will figure it out the correct label. Introducing new fields will just now require you to change your viewmodels, adding a new entry to the Dictionary.
It's not perfect, but I think it's an okay way to solve it. I'm by far not an MVC expert so maybe others can come up with a more natural way to do it as well. I also prepared a working sample app and pushed to GitHub. You can check it out here: aspnet-view-component-demo. Hope it helps somehow.

Get values from subobject in Razor HTML

I have a class that contains a super object. When creating this class, it will contain one of the subobjects of the superobject. However, because the attributes of the subobjects are not in the superobject, I cannot access these attributes in Razor. Can someone tell me how I can reach the subobjects' attributes? I cannot put the subobjects' attributes in the superobject, because I'm going to convert the class to json and I cannot have all attributes visible in the json.
Class:
public class FoundPattern
{
public Pattern Pattern = new Pattern();
}
Superobject:
public class Pattern : OntologicalModel
{
}
Subobjects:
public class KpiPattern : Pattern
{
public List<KPI> KPIs = new List<KPI>();
}
.
public class ProcessPattern : Pattern
{
public List<Process> Processes = new List<Process>();
}
Razor page:
#model IEnumerable<FoundPattern>
#foreach (var x in item.SUBOBJECTNAME.SUBOBJECTATTRIBUTE)
{
// do something
}
Instead of SUBOBJECTNAME and SUBOBJECTATTRIBUTE I need the subobject and subobject attribute, respectively.
Should be something like
#foreach (var x in Model)
{
if(x.Pattern is ProcessPattern) {
foreach (var y in ((KpiPattern)x.Pattern).Processes)
{
//enter code here
}
}
if(x.Pattern is KpiPattern) {
foreach (var y in ((KpiPattern)x.Pattern).KPIs)
{
//enter code here
}
}
}
If you don't have intellisense or the view is showing syntax errors then a possible issue is that you didn't include the namespace of your classes in your web.config in your Views folder.
Remember to open and close the view after editing the web.config for the intellisense to start working correctly.

How to initialise BaseModel in BaseController

I have a class of properties which are set from a service which I need available on every view of my MVC application.
Therefore I've created a "Base View Model" which my view models will inherit from.
public class BaseModel
{
public BaseModel()
{
foo = "foo value";
bar = "bar value";
}
public string foo { get; set; }
public string bar { get; set; }
}
public class HomeIndexViewModel : BaseModel
{
}
I have then created a "Base Controller" which all my controllers will inherit from:
public class BaseController : Controller
{
public BaseController()
{
}
}
public class HomeController : BaseController
{
public ActionResult Index()
{
HomeIndexViewModel model = new HomeIndexViewModel();
return View(model);
}
}
This is working as expected and I can call #Model.foo in my view and get foo value.
However I don't believe I should be initialising the values of BaseModel in it's constructor as this isn't using Dependency Injection and will become difficult to unit test.
How can I move the setting of the values foo and bar into the BaseController?
Of course I could set the values in the HomeController, but I would rather abstract this from the controller as the logic will always be the same and would bloat all my controllers.
I think the problem is that you are creating the instance of your models inside of the action, so the base controller has no reference to the object to set the properties.
Personally I would probably opt for some 'factory-type' function in the base controller that is responsible for creating the models as you need them.
Something like this for example:
public class BaseController : Controller
{
public T CreateBaseModel<T>() where T : BaseModel, new()
{
return new T
{
foo = "foo value",
bar = "bar value"
};
}
}
Then when you create your models in the actions you can do them like this:
HomeIndexViewModel model = CreateBaseModel<HomeIndexViewModel>();
If for some reason you need to pass parameters to your model constructor then you can have an overload like this:
public T CreateBaseModel<T>(params object[] args) where T : BaseModel
{
T model = (T)Activator.CreateInstance(typeof(T), args);
model.foo = "foo";
return model;
}
HomeIndexViewModel model = CreateBaseModel<HomeIndexViewModel>(param1, param2, etc);
Alternative
The main benefit of the above method is that you can access the foo and bar properties within the action code. However, if you don't care about this and only need the values to be accessible from within the View page, then you can override the OnActionExecuted method and apply the values in there. The benefit of this approach is that you don't need to change the way your models are created in the actions...
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
BaseModel model = filterContext.Controller.ViewData.Model as BaseModel;
if (model != null)
{
model.foo = "foo value";
model.bar = "bar value";
}
base.OnActionExecuted(filterContext);
}
Having the null check in there means it will only try to apply the values for models that inherit from BaseModel, which means you can still use other models without worry.
With this approach, your action code goes back to how it was originally:
HomeIndexViewModel model = new HomeIndexViewModel();
return View(model);

Which is the best way to use multiple models with sames properties and using a unique view?

For example, I have 3 models with an Id (int), a Name (string) and an active (bool). It's possible to use just one view for these models with the same properties ? A technique like a generic object ? Inheritance ? It's to avoid to write multiple views with the same HTML code.
You could create a ViewModel.
For sample:
public class CustomerViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool Active { get; set; }
}
And create another ViewModel with a type like this:
public class CustomersViewModel
{
public CustomerViewModel Customer1 { get; set; }
public CustomerViewModel Customer2 { get; set; }
public CustomerViewModel Customer3 { get; set; }
}
and type your view with this type:
#model CustomersViewModel
Or just use an collection to type your view
#model List<CustomerViewModel>
Take a look at this anwser!
https://stackoverflow.com/a/694179/316799
In a view you can either
specify shared base class for all models and use that.
use dynamic as model
split view in shared (extract into separate view) and specific part. Than call shared sub-view with either existing model (if using base class/dynamic) or simply new model constructed based on data in specific model.
Sample of extracting shared portion with inheritance. Using Html.Partial to render shared portion:
class SharedModel { /* Id,...*/ }
class SpecificModel : SharedModel { /* Special... */ }
SpecificView:
#model SpecificModel
#Html.Partial("SharedView", Model)
<div>#Model.Special</div>
SharedView:
#model SharedModel
<div>#Model.Id</div>
Side note: You can specify view name when returning result by using View if view name does not match action name:
return View("MySharedView", model);
In ASP.NET MVC4 you have the opportunity not to define a model for the view. This means leave the definition of the model empty (don't use #model MyNamespace.MyClass) and then it will automatically use "dynamic" as model.
Greetings
Christian

Can you cast down when passing a model to a view?

Lets say I have two classes one base class:
public class BaseModel {
}
And a couple of children:
public class FooModel : BaseModel {
}
public class BarModel : BaseModel {
}
Now my view I would like to expect a model like this:
#model IList<BaseModel>
So that I can edit these models on one page.
However when I pass in a collection of these models in the action I get an exception saying:
The model item passed into the dictionary is of type
'System.Collections.Generic.List1[BarModel]', but this dictionary
requires a model item of type
'System.Collections.Generic.IList1[BaseModel]'.
Like this:
var models = new List<BaseModel>(){ new BarModel(), new FooModel ()}
return View(models);
How can I achieve this?
Thanks
Assuming you're not modify the model collection on the page, your page shouldn't take IList<T>. Rather it should take IEnumerable<T>:
#model IEnumerable<BaseModel>
That should take care of things.
You can alseo use KnownType:
[Serilizable]
[KnownType(typeof(FooModel))]
[KnownType(typeof(BarModel))]
public class BaseModel
{
}
Hope this helps.

Categories

Resources