Friday, January 26, 2007

Playing with attributes

I'm sure by now you've seen all sorts of .NET framework attributes that can be applied to various pieces of code. The most notable of these in my code are FlagsAttribute (for enums), and the XML serialization / Web Services ones (XmlElementAttribute, XmlAttributeAttribute, etc., and WebServiceAttribute). These are very nice and unleash all kinds of built-in behavior in the framework, but did you know you can make your own attributes and use them for your own purposes? That's what this blog entry is all about!

For me, one of the most common problems I face is one in which I would like to use an enumeration for a class member that has a list of choices. Of course, this is a no-brainer to use an enum for this, but often I want to go one step further. Often, I'd like to limit the enum's choices for the class member, based on the value of one or more of the other class members. This problem is very common in heirarchies where you have a shared base class that you want to support a member in that can have values that aren't always applicable to the object.

Let's say for the sake of argument that you have a class A with members x, y, and z. The z member should be a choice of z1, z2, or z3, but each of these choices is only available when the following conditions hold:


ConditionAvailability
x > 0, y anythingz1 z2 available, z3 not
x = 0, y > 0z1 available, z2, z3 not
x = 0, y < 0z2 available, z1, z3 not
x < 0, y anythingz3 available, z1, z2 not

We can see that these rules can be distilled into a separate "test function" for each value of z. For z1, it's available if x>0 (x == 0 && y>0). For z2, it's available if x>0 (x == 0 && y<0). For z3, it's available if x<0 only.

Now, I crashed my compiler in the process of trying to use anonymous delegates to express these conditions. Unfortunately, that doesn't seem to work. However, we can package these three tests into a helper class that provides static methods to perform these tests. For instance, let's make a class ZValueTests as follows:

  public static class ZValueTests
{
public static bool TestForZ1(int x, int y)
{
return x>0 (x == 0 && y>0);
}

public static bool TestForZ2(int x, int y)
{
return x>0 (x == 0 && y<0);
}

public static bool TestForZ3(int x, int y)
{
return x<0;
}
}

Now that we have these tests, we can use them in an attribute that we apply to each of our enum values. Let's first define our attribute class. We'll call it ValidForAttribute, and it must derive from the Attribute class to be known by the compiler. We'll have to use reflection to call the test functions, so this will be a fun demo in many ways! Here's our attribute declaration.

  [AttributeUsage(AttributeTargets.Field)]
public class ValidForAttribute : Attribute
{
private SR.MethodInfo _Test;

public ValidForAttribute(
Type TestClass,
string MethodName)
{
SR.MethodInfo mi
= TestClass.GetMethod(MethodName);
// assertions omitted for brevity
_Test = mi;
}

public bool Test(int x, int y)
{
return (bool)_Test.Invoke(
null,
new object[] { x, y });
}
}

As you can see from the declaration, we'll need to specify our attribute using the type of the class that implements our tests, and the name of the method on that class. This is because we can't use non-constant expressions in the attribute initializer (a pretty big limitation if you ask me), so we can't even use delegate creation expressions, much less anonymous methods! Anyway, this works, it's just a little less pleasant than I would have liked.

Now, we need to declare our actual Enum. You'll see it will be very easy and will look something like the following:

  public enum ZValue
{
[ValidFor(typeof(ZValueTests), "TestForZ1")]
z1,
[ValidFor(typeof(ZValueTests), "TestForZ2")]
z2,
[ValidFor(typeof(ZValueTests), "TestForZ3")]
z3
}

This is very pleasant as it very clearly defines the test functions that apply to each z value. Of course, one unpleasant thing is that the types of these functions cannot be checked until runtime (that's what the asserts that are omitted from the code in the attribute do). I would recommend even throwing an exception if the function isn't declared right from the constructor of the attribute, but that may be a bit harsh.

Now, I've shown how you can declare the enums, attribute class, and test class - but what we really want to see is the validation in action, right? So, here we go. If we declare the class A as follows:

public class A {
ZValue _z;

bool ValidateZ(int x, int y, ZValue z) {
SR.FieldInfo fi = typeof(ZValue)
.GetField(Enum.GetName(
typeof(ZValue), z));
// test all attrs (ValidForAttribute)
if(fi != null)
foreach(ValidForAttribute vfa
in fi.GetCustomAttributes(
typeof(ValidForAttribute),
true))
if (vfa.Test(x, y))
return true;
// ret false if enum not found,
// or no attrs found returning true
return false;
}

public int x, y;
public ZValue z
{
get { return _z; }
set {
if(ValidateZ(x, y, value))
_z = value;
else
throw new Exception(
string.Format(
"Invalid z: ({0}, {1}, {2})",
x, y, z));
}
}
}

then the code will validate a new value of z against x and y before setting the internal field (_z) to that value. An exception will be raised if the value is invalid. This may seem like a lot of work, instead of just validating z inside the z "set" accessor. That's true, it is. However, the real advantage to this is that the validation code can be used in other ways. For instance, consider if we wanted a list of valid values for z (for a UI, for instance) given a particular value of x and y. We can write the following code:

public static ZValue[] ValidZs(int x, int y) {
List<ZValue> zv = new List<ZValue>(
(IEnumerable<ZValue>)
Enum.GetValues(typeof(ZValue)));
zv.RemoveAll(new Predicate<ZValue>(
delegate(ZValue v)
{
SR.FieldInfo fi = typeof(ZValue)
.GetField(Enum.GetName(
typeof(ZValue),
v
));
if (fi != null)
foreach (ValidForAttribute vfa
in fi.GetCustomAttributes(
typeof(ValidForAttribute),
true))
if (vfa.Test(x, y))
return false;
return true;
}
));
}

Hope you found this interesting! Email with questions, if you have them!

No comments: