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

4.9k Views Asked by At

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?

1

There are 1 best solutions below

2
On BEST ANSWER

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.