Patrick
Patrick

Reputation: 1783

C# attribute text from resource file?

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?

Upvotes: 34

Views: 26545

Answers (9)

Bahram Ghahari
Bahram Ghahari

Reputation: 86

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

Upvotes: 3

campo
campo

Reputation: 1902

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

Upvotes: 3

Steve Andrews
Steve Andrews

Reputation: 559

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

Upvotes: 1

Peter Lillevold
Peter Lillevold

Reputation: 33940

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.

Upvotes: 1

Jesus is Lord
Jesus is Lord

Reputation: 15409

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.

Upvotes: 0

Peter
Peter

Reputation: 9712

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

Upvotes: 10

Peter Starbek
Peter Starbek

Reputation: 713

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

Upvotes: 31

Jon Skeet
Jon Skeet

Reputation: 1502716

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.

Upvotes: 26

John Saunders
John Saunders

Reputation: 161821

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

Upvotes: 1

Related Questions