Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to change ViewModel display name without using `DisplayNameAttribute`?

I want to change the display name of some properties (of ViewModel) directly without using [DisplayName("prop name")]. This should happen either directly inside the controller before returning the View, or inside the ViewModel class itself.

I do NOT want to change anything in the View, and I do not want to use any data annotations. How can I achieve that?

Is there any fluent syntax maybe to get that?

I am using: ASP.Net Core 2.0

The problem with data annotations is that I want to get my display name in run time (while data annotations are pre-compiled).

UPDATE:

The main reason for asking this question was to find a way to wrap the IStringLocalizer and particularly its behavior when localizing data annotations. The accepted answer explains the basics of that well.

like image 579
Mohammed Noureldin Avatar asked Jan 29 '23 23:01

Mohammed Noureldin


1 Answers

@Tseng, sorry I should have said that more clearly, I meant that we should use either naming convention, or SharedResources. but not both, I have many cases where I have a lot of shared resources, and many ViewModel-specific strings (so a mixture). That is not achievable with .Net Core localization solution.

If your only worries is that you can or can't determine if one or multiple resource files are chosen, that can easily be configured. I had to dig a bit in the source code, bit its seems possible.

As we can see here the localizer is determined by the factory defined in the configuration

if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null)
{
    localizer = _localizationOptions.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory);
}

whereas _localizationOptions is MvcDataAnnotationsLocalizationOptions.

The default implementation of MvcDataAnnotationsLocalizationOptions is here:

/// <inheritdoc />
public void Configure(MvcDataAnnotationsLocalizationOptions options)
{
    if (options == null)
    {
        throw new ArgumentNullException(nameof(options));
    }

    options.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) =>
        stringLocalizerFactory.Create(modelType);
}

So it uses per model resources by default.

You can change that to a SharedResource file for all data annotations if you like, with the following in your Startup.ConfigureServices (untested, but should work):

services.AddMvc()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
            factory.Create(typeof(SharedResource));
    });

This will effectively ignore the passed type and always return a shared string localizer.

Of course, you can add any logic there and decide on type-per-type case which localizer you are going to use.

Edit

If that's not enough, you can implement your own custom IDisplayMetadataProvider which handles it the way you want. But using the DisplayAttribute should be enough actually. DisplayAttribute has additional parameters which allow you to define the resource type.

[Display(Name = "StringToLocalize", ResourceType = typeof(SharedResource))]

With the ResourceType you can choose the class (and hence the resource file name) used to look up for the localization.

Edit 2: Using a wrapped IStringLocalizer with fallback to per-viewmodel resource

The more elegant solution involves using the above MvcDataAnnotationsLocalizationOptions options file to return your own IStringLocalizer which looks into one resource file and falls back to the other one.

public class DataAnnotationStringLocalizer : IStringLocalizer
{
    private readonly IStringLocalizer primaryLocalizer;
    private readonly IStringLocalizer fallbackLocalizer;

    public DataAnnotationStringLocalizer(IStringLocalizer primaryLocalizer, IStringLocalizer fallbackLocalizer)
    {
        this.primaryLocalizer = primaryLocalizer ?? throw new ArgumentNullException(nameof(primaryLocalizer));
        this.fallbackLocalizer = fallbackLocalizer ?? throw new ArgumentNullException(nameof(fallbackLocalizer));
    }

    public LocalizedString this[string name]
    {
        get
        {
            LocalizedString localizedString = primaryLocalizer[name];
            if (localizedString.ResourceNotFound)
            {
                localizedString = fallbackLocalizer[name];
            }

            return localizedString;
        }
    }

    public LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            LocalizedString localizedString = primaryLocalizer[name, arguments];
            if (localizedString.ResourceNotFound)
            {
                localizedString = fallbackLocalizer[name, arguments];
            }

            return localizedString;
        }
    }

    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        => primaryLocalizer.GetAllStrings(includeParentCultures).Concat(fallbackLocalizer.GetAllStrings(includeParentCultures));

    public IStringLocalizer WithCulture(CultureInfo culture)
        => new DataAnnotationStringLocalizer(primaryLocalizer.WithCulture(culture), fallbackLocalizer.WithCulture(culture));
}

And with the following options

services.AddMvc()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
        {
            return new DataAnnotationStringLocalizer(
                factory?.Create(typeof(SharedResource)),
                factory?.Create(type)
            );
        };
    });

Now, the string is first resolved from the shared resource and if the string wasn't found there, it will resolve it from the view model type (type parameter passed to the factory method).

If you don't like the logic and you want that it first looks into the view-model resource files, you just change the order to

services.AddMvc()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
        {
            return new DataAnnotationStringLocalizer(
                factory?.Create(type),
                factory?.Create(typeof(SharedResource))
            );
        }
    });

Now the view model is the primary resolver and shared resource the secondary

like image 152
Tseng Avatar answered Jan 31 '23 12:01

Tseng