34

I have an attribute and i want to load text to the attribute from a resource file.

[IntegerValidation(1, 70, ErrorMessage = Data.Messages.Speed)]
private int i_Speed;

But I keep getting "An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type"

It works perfectly if i add a string instead of Data.Messages.Text, like:

[IntegerValidation(1, 70, ErrorMessage = "Invalid max speed")]

Any ideas?

DaveShaw
  • 52,123
  • 16
  • 112
  • 141
Patrick
  • 1,763
  • 5
  • 18
  • 16
  • Here are the steps to bring data from Resource files 1 - Add Resource File name it for example Captions.resx 2 - Add some string with their values like FirstName 3 - Note: You may get an error “Cannot retrieve property 'FirstName' because localization failed. Type 'Xxxx.Captions' is not public or does not contain a public static string property with the name 'FirstName'.“. This is because, by default the resource file properties have access modifier set to internal. Change it to Public in the Access Modifier dropdown in the resource file toolbar. 4 - Here is the code – Asad Naeem Mar 12 '18 at 07:11
  • 4 - Here is the code [Required(ErrorMessageResourceName = "RequiredFirstName", ErrorMessageResourceType = typeof(Resources.Captions))] //[Required(ErrorMessage = "Please Enter System Name")] [Display(Name = "FirstName", ResourceType = typeof(Resources.Captions))] public string SystemName { get; set; } – Asad Naeem Mar 12 '18 at 07:19

9 Answers9

30

Here is my solution. I've added resourceName and resourceType properties to attribute, like microsoft has done in DataAnnotations.

public class CustomAttribute : Attribute
{

    public CustomAttribute(Type resourceType, string resourceName)
    {
            Message = ResourceHelper.GetResourceLookup(resourceType, resourceName);
    }

    public string Message { get; set; }
}

public class ResourceHelper
{
    public static  string GetResourceLookup(Type resourceType, string resourceName)
    {
        if ((resourceType != null) && (resourceName != null))
        {
            PropertyInfo property = resourceType.GetProperty(resourceName, BindingFlags.Public | BindingFlags.Static);
            if (property == null)
            {
                throw new InvalidOperationException(string.Format("Resource Type Does Not Have Property"));
            }
            if (property.PropertyType != typeof(string))
            {
                throw new InvalidOperationException(string.Format("Resource Property is Not String Type"));
            }
            return (string)property.GetValue(null, null);
        }
        return null; 
    }
}
Peter Starbek
  • 703
  • 8
  • 12
  • 1
    I made this method generic and added NonPublic to the Binding list, so I could pull resources out of other dll's, and it works great. I am actually using it to pull image resources. – Peter Sep 10 '12 at 03:29
  • @Peter - could you post your modified solution? – Andrew Mar 03 '13 at 23:58
  • @Andrew - I'm using this in an open source project. You'll have to dig a little but here is the link to the attribute class: http://pulse.codeplex.com/SourceControl/changeset/view/71556#1475612 – Peter Mar 04 '13 at 03:07
  • @Andrew - I went ahead and posted it as a separate answer also. – Peter Mar 04 '13 at 03:11
26

Attribute values are hard-coded into the assembly when you compile. If you want to do anything at execution time, you'll need to use a constant as the key, then put some code into the attribute class itself to load the resource.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
10

Here is the modified version of the one I put together:

[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
public class ProviderIconAttribute : Attribute
{
    public Image ProviderIcon { get; protected set; }

    public ProviderIconAttribute(Type resourceType, string resourceName)
    {
        var value = ResourceHelper.GetResourceLookup<Image>(resourceType, resourceName);

        this.ProviderIcon = value;
    }
}

    //From http://stackoverflow.com/questions/1150874/c-sharp-attribute-text-from-resource-file
    //Only thing I changed was adding NonPublic to binding flags since our images come from other dll's
    // and making it generic, as the original only supports strings
    public class ResourceHelper
    {
        public static T GetResourceLookup<T>(Type resourceType, string resourceName)
        {
            if ((resourceType != null) && (resourceName != null))
            {
                PropertyInfo property = resourceType.GetProperty(resourceName, BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
                if (property == null)
                {
                    return default(T);
                }

                return (T)property.GetValue(null, null);
            }
            return default(T);
        }
    }
Peter
  • 9,643
  • 6
  • 61
  • 108
3

I came across this problem with the display name for attribute, and I made the following changes:

For our resource file I changed the custom tool property to PublicResXFileCodeGenerator

Then added this to the attribute:

[Display(Name = "MyResourceName", ResourceType = typeof(Resources.MyResources))]
campo
  • 1,872
  • 1
  • 17
  • 19
3

If you're using .NET 3.5 or newer you can use ErrorMessageResourceName and ErrorMessageResourceType parameters.

For example [Required(ErrorMessageResourceName ="attribute_name" , ErrorMessageResourceType = typeof(resource_file_type))]

1

Use a string which is the name of the resource. .NET does this with some internal attributes.

John Saunders
  • 160,644
  • 26
  • 247
  • 397
  • 1
    [RegularExpression(".+@.+\\..+", ErrorMessageResourceName = "InvalidEmail", ErrorMessageResourceType=typeof(Resources.Registration))] – fireydude May 14 '15 at 10:24
1

The nature of attributes is such that the data you put in attribute properties must be constants. These values will be stored within an assembly, but will never result in compiled code that is executed. Thus you cannot have attribute values that rely on being executed in order to calculate the results.

Peter Lillevold
  • 33,668
  • 7
  • 97
  • 131
1

I have a similar case, where I need to put resource strings into attributes. In C# 6, we have the nameof() capability, and that seems to do the trick.

In my case, I can use [SomeAttribute(nameof(Resources.SomeResourceKey))] and it compiles fine. Then I just have to do a little work on the other end to use that value to get the correct string from the Resources file.

In your case, you might try:

[IntegerValidation(1, 70, ErrorMessageResourceKey = nameof(Data.Messages.Speed))]
private int i_Speed;

Then you can do something along the lines of (pseudo code):

Properties.Resources.ResourceManager.GetString(attribute.ErrorMessageResourceKey);
Steve Andrews
  • 561
  • 3
  • 14
0

Here's something I wrote since I couldn't find anything else that does this.:

Input

Write a constant string class in project A.

[GenerateResource]
public static class ResourceFileName
{
    public static class ThisSupports
    {
        public static class NestedClasses
        {
            [Comment("Comment value")]
            public const string ResourceKey = "Resource Value";
        }
    }
}

Output

And a resource will be generated in the project that contains the constants class.

enter image description here

All you need to do is have this code somewhere:

Source

public class CommentAttribute : Attribute
{
    public CommentAttribute(string comment)
    {
        this.Comment = comment;
    }

    public string Comment { get; set; }
}

public class GenerateResourceAttribute : Attribute
{
    public string FileName { get; set; }
}

public class ResourceGenerator
{
    public ResourceGenerator(IEnumerable<Assembly> assemblies)
    {
        // Loop over the provided assemblies.
        foreach (var assembly in assemblies)
        {
            // Loop over each type in the assembly.
            foreach (var type in assembly.GetTypes())
            {
                // See if the type has the GenerateResource attribute.
                var attribute = type.GetCustomAttribute<GenerateResourceAttribute>(false);
                if (attribute != null)
                {
                    // If so determine the output directory.  First assume it's the current directory.
                    var outputDirectory = Directory.GetCurrentDirectory();

                    // Is this assembly part of the output directory?
                    var index = outputDirectory.LastIndexOf(typeof(ResourceGenerator).Assembly.GetName().Name);
                    if (index >= 0)
                    {
                        // If so remove it and anything after it.
                        outputDirectory = outputDirectory.Substring(0, index);

                        // Is the concatenation of the output directory and the target assembly name not a directory?
                        outputDirectory = Path.Combine(outputDirectory, type.Assembly.GetName().Name);
                        if (!Directory.Exists(outputDirectory))
                        {
                            // If that is the case make it the current directory. 
                            outputDirectory = Directory.GetCurrentDirectory();
                        }
                    }

                    // Use the default file name (Type + "Resources") if one was not provided.
                    var fileName = attribute.FileName;
                    if (fileName == null)
                    {
                        fileName = type.Name + "Resources";
                    }

                    // Add .resx to the end of the file name.
                    fileName = Path.Combine(outputDirectory, fileName);
                    if (!fileName.EndsWith(".resx", StringComparison.InvariantCultureIgnoreCase))
                    {
                        fileName += ".resx";
                    }

                    using (var resx = new ResXResourceWriter(fileName))
                    {
                        var tuples = this.GetTuplesRecursive("", type).OrderBy(t => t.Item1);
                        foreach (var tuple in tuples)
                        {
                            var key = tuple.Item1 + tuple.Item2.Name;

                            var value = tuple.Item2.GetValue(null);

                            string comment = null;
                            var commentAttribute = tuple.Item2.GetCustomAttribute<CommentAttribute>();
                            if (commentAttribute != null)
                            {
                                comment = commentAttribute.Comment;
                            }

                            resx.AddResource(new ResXDataNode(key, value) { Comment = comment });
                        }
                    }
                }
            }
        }
    }

    private IEnumerable<Tuple<string, FieldInfo>> GetTuplesRecursive(string prefix, Type type)
    {
        // Get the properties for the current type.
        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static))
        {
            yield return new Tuple<string, FieldInfo>(prefix, field);
        }

        // Get the properties for each child type.
        foreach (var nestedType in type.GetNestedTypes())
        {
            foreach (var tuple in this.GetTuplesRecursive(prefix + nestedType.Name, nestedType))
            {
                yield return tuple;
            }
        }
    }
}

And then make a small project that has a reference to all your assemblies with [GenerateResource]

public class Program
{
    static void Main(string[] args)
    {
        var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
        string path = Directory.GetCurrentDirectory();
        foreach (string dll in Directory.GetFiles(path, "*.dll"))
        {
            assemblies.Add(Assembly.LoadFile(dll));
        }
        assemblies = assemblies.Distinct().ToList();

        new ResourceGenerator(assemblies);
    }
}

Then your attributes can use the static class ResourceFileName.ThisSupports.NestedClasses.ResourceKey while other code can use the resource file.

You might need to tailor it to your specific needs.

Jesus is Lord
  • 14,971
  • 11
  • 66
  • 97