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

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.

Related

.NET MVC - Collection properties should be read only Change to be read-only by removing the property

I have a controller that uses two classes. One is called IndexModel and the other IndexViewModel.
I pass the IndexViewModel into the IndexModel constructor.
[HttpGet]
public ActionResult Index()
{
var model = new IndexModel(new IndexViewModel());
var vm = model.GetViewModel();
return View("Index", vm);
}
Here is the view model class. Notice that the setter is private.
public class IndexViewModel
{
public IList<SelectListItem> SelectListItems { get; private set; }
public IndexViewModel()
{
this.SelectListItems = new List<SelectListItem>();
}
}
Here is the Model. When GetViewModel is called the SelectListItems list is populated and the view model returned.
public class IndexModel
{
private IndexViewModel vm;
public IndexModel(IndexViewModel IndexViewModel)
{
this.vm = IndexViewModel;
}
public IndexViewModel GetViewModel()
{
this.FillSelectListItems();
return vm;
}
private void FillSelectListItems()
{
// this data is pulled in from another class that queries a database...
var data = ...
foreach (var itm in data)
{
vm.SelectListItems.Add(new SelectListItem()
{
Value = itm.Id.ToString(),
Text = itm.Text,
});
}
}
}
I would appreciate any comments on how this is currently structured, but my main questions are:
Is it bad practice to write a bunch of methods, like FillSelectListItems(), that alter collection data and don't return a result?
Should I make the setter public so I can just return a list from my method and set the view model property that way?
What do you have to gain by making it private? A headache... make it public :)
There aren't any problems using view models in other view models... Imagine having a blog post... BlogPostViewModel... you would expect it to also have comments right? BlogPostViewModel > CommentViewModel
public class BlogPostViewModel
{
public string BlogPostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<CommentViewModel> Comments { get; set; }
}
Now when you render that, on your PostsController, at Posts/{id}, the view Posts/Index.cshtml would be rendered, and your comments can be rendered inside a partial view...
// Take this as pseudo code as there's some syntatic errors (can't be asked to open VS)
#for(int i = ... i < Model.Comments.Length... i++){
this.Html.Partial("_CommentsPartial.cshtml, Model.Comments[i])
}
On another note, if you wanted, you could pass your Model to the view as a JSON object as well without with this neat little hack... In your controller action...
this.ViewBag.Json = JsonConvert.SerializeObject<TViewModel>(viewModel);
And in your view just pick it backup...
<script> var json = #this.ViewBag.Json </script>
Hopefully this has provided some insight with regards to the purpose these View Models serve...

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);

Polymorphic ViewModel collection and rendering in MVC partial Views

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

Where should I place my domain model to view model mapping code?

Currently I use private static methods in my controller file to map domain model to view model and vice-versa. Like below:
public ActionResult Details(int personID)
{
Person personDM = service.Get(personID);
PersonViewModel personVM = MapDmToVm(personDM);
return View(personVM);
}
private static PersonViewModel MapDmToVm(Person dm)
{
PersonViewModel vm;
// Map to VM
return vm;
}
Is there any other standard way to do this?
I prefer to put the mapping logic inside the view model (dto) class, because we want to keep the domain model as clean as possible and also the domain model might change overtime.
public class Person
{
public string Name {get; set;}
}
public class PersonViewModel
{
public string Text {get; set;}
public static implicit operator PersonViewModel(Person dm)
{
var vm = new PersonViewModel { Text = dm.Name };
return vm;
}
public static implicit operator Person(PersonViewModel vm)
{
var dm = new Person { Name = vm.Text };
return dm;
}
}
and use it in the controller without explicit casting.
Person dm = service.Get(id);
PersonViewModel vm = dm;
Since the mapping is not always trivial, I think that it might be better to separate it into a different class other than the viewmodel.
That way each class has its own single responsibility. You might want to add an extension method to your domain model, something like:
public static MyViewModel ToViewModel(this MyDomainModel model)
{
// mapping code goes here
}
You also might consider using automapper and call its Map method from your controller.

Casting Object depending on its type

I made a validation method for business rules that are not verified from my rules in the model, but I'm having a problem to make it work. Since there are two possible scenarios(customer or seller registration), they need to be treated separately in their own views and models. The seller registration inherits from customer registration for the basic info, so all fields in customer are also in seller. But since I'm working with 2 different models, even though both have the same fields that I'm doing the validation upon, I needed to use Object to use the same validation method. But unfortunately I'm having trouble to do so.
[CustomHandleError]
private bool ValidateRegistrationForm (Object registerViewModelObject) {
if (registerViewModelObject is RegisterViewModel)
{
RegisterViewModel registerViewModel =
(RegisterViewModel)registerViewModelObject;
}
else
{
RegisterSellerViewModel registerViewModel =
(RegisterSellerViewModel)registerViewModelObject;
}
if (ModelState.IsValid)
{
string [] names = registerViewModel.Name.Split (
new string [] {" "}, StringSplitOptions.RemoveEmptyEntries);
if (names.Length == 1)
ModelState.AddModelError ("Name", "Fill your full name");
if (CustomerUtilities.IsCpf (registerViewModel.Identity) == false)
ModelState.AddModelError ("Identity", "Invalid CPF value");
if (this.AuthenticatorService.IsExistentUser (registerViewModel.Email))
ModelState.AddModelError ("Email", "Email already registered");
}
}
As you can see, after the if (ModelState.IsValid) the IntelliSense doesn't find registerViewModel in the current context. I wonder why this happens, since that variable is defined inside the if AND the else above, so there is no way to reach that code without it being defined.
Is there any workaround for this(other than creating a new method or passing 2 variables)?
Declare RegisterViewModel outside of the if block scope, and assign it within the if block.
RegisterViewModel registerViewModel;
if (registerViewModelObject is RegisterViewMOdel)
{
registerViewModel = // ...
}
else
{
registerViewModel = // ...
}
If you wish to have two separate variables then declare both outside of the if statement and test for null after.
RegisterViewModel registerViewModel;
RegisterSellerViewModel sellerModel;
if (registerViewModelObject is RegisterViewModel)
{
registerViewModel = (RegisterViewModel)registerViewModelObject;
}
else
{
sellerViewModel = (RegisterSellerViewModel)registerViewModelObject;
}
However, defining an interface to use instead of Object would be the better option.
public interface IRegisterViewModel
{
public string Name { get; set;}
public ... Identity {get; set;}
...
}
public class RegisterViewModel : IRegisterViewModel
{
...
}
public class RegisterSellerViewModel : IRegisterViewModel
{
...
}
Then use ValidateRegistrationForm(IRegisterViewModel registerViewModel) and you can get rid of the if statement entirely.
You should define RegisterViewModel outside from your if statemtent. And make assignment inside your if statement.
Like;
RegisterViewModel registerViewModel;
if(...)
{
//make your assigment here.
}
You probably need to extract methods that are common for RegisterViewModel and RegisterSellerViewModel into an interface and implement it in both classes. Then cast registerViewModelObject to this interface regardless of its actual type.
The problem occurs since you don't have a single variable defined in the main scope of the function. In the way that you have written your code, you define two variables that are inside different scopes.
How I would go about with the solution:
I would make a base class.
class RegisterModel
{
public string Name;
public IdentifyType Identify;
public string Email;
}
And then both your classes can inherit from the base class. Like this:
class RegisterViewModel
: RegisterModel
{...}
class RegisterSellerViewModel
: RegisterModel
{...}
Now you can actually covert the Object variable in your function a single time. Like this:
private bool Validate(Object viewModel)
{
var castViewModel = (RegisterModel)viewModel;
if(ModelState.IsValid)
{
...
}
}
Note that this will cause a run-time error if viewModel is not of type RegisterModel.

Categories

Resources