in

ASP.NET Weblogs

Andy Smith's Blog

Page.RegisterStartupScript('Andy', 'MetaBuilders_WebControls_GainKnowledge();');

Expandable pseudo enums

I like System.Enum. Really. But sometimes it's just not enough. And hey, I also liked Don's neato article about extending enums with attributes. But even that doesn't cover all my bases sometimes.

Here's the scenario... I have a property that seems at first to ask for an enum. The value of the property can be one of a few values. The real value is just a number, but it has a common name. But then I realize, hey, I want more than just a progmattic name to a numeric value, I actually want the number, a common name, and a friendlier name. Ok, you could do that with attributes on the enums, specifying the additional friendly name. But then... boom... I realize that I'll actually only know most of the possible values for the property. There is apparently a few more undocumented values that could appear randomly out in the wild, and I don't want the whole thing to be incompatible just because of some random extra value. However, I do want to keep the nice MyEnum.Foo kind of usage for the majority case. So... I figured I'd use the almost-well-known Typesafe Enum Pattern made almost popular with that J-language, with a few modifications for .net and my purposes, of course.

So ya... here's an example extendable enum. I'll talk about the neat bits after the code:

using System;
using System.Runtime.Serialization;
using System.Collections;
[Serializable]
public sealed class MyExpandableEnum : System.MarshalByRefObject,  ISerializable {
 #region Defined values
 public static readonly MyExpandableEnum Foo = new MyExpandableEnum("Foo",1);
 public static readonly MyExpandableEnum Bar = new MyExpandableEnum("Bar",2);
 public static readonly MyExpandableEnum Zap = new MyExpandableEnum("Zap",3);
 #endregion
  
 private MyExpandableEnum(String description, Int32 number) :
  this(description,number,true) {
 }
 private MyExpandableEnum(String description, Int32 number, Boolean isDefined ) {
  if ( definedItems == null ) {
   definedItems = new ArrayList();
  }
  if ( newItems == null ) {
   newItems = new ArrayList();
  }
  this.description = description;
  this.number = number;
  if ( isDefined ) {
   definedItems.Add(this);
  }
 }
 public static MyExpandableEnum GetInstance( String description, Int32 number ) {
  foreach( MyExpandableEnum definedItem in definedItems ) {
   if ( definedItem.number == number ) {
    return definedItem;
   }
  }
  
  foreach( MyExpandableEnum newItem in newItems ) {
   if ( newItem.number == number ) {
    return newItem;
   }
  }
  MyExpandableEnum newRef = new MyExpandableEnum(description, number, false);
  newItems.Add(newRef);
  return newRef;
 }
 public static Boolean IsDefined(MyExpandableEnum item) {
  foreach( MyExpandableEnum definedItem in definedItems ) {
   if ( definedItem.Equals(item) ) {
    return true;
   }
  }
  return false;
 }
 public override String ToString() {
  return this.description;
 }
 public String Description {
  get {
   return description;
  }
 }
 public Int32 Number {
  get {
   return number;
  }
 }
 private static ArrayList newItems;
 private static ArrayList definedItems;
 private String description;
 private Int32 number;
 #region ISerializable
 public void GetObjectData(SerializationInfo info, StreamingContext context) {
  info.SetType(typeof(MyExpandableEnumProxy));
  info.AddValue("Description",this.description);
  info.AddValue("Number",this.number);
 }
 [Serializable]
 private sealed class MyExpandableEnumProxy : IObjectReference, ISerializable {
  private MyExpandableEnumProxy( SerializationInfo info, StreamingContext context ) {
   this.description = info.GetString("Description");
   this.number = info.GetInt32("Number");
  }
  private String description = "";
  private Int32 number;
  public object GetRealObject(StreamingContext context) {
   return MyExpandableEnum.GetInstance(this.description,this.number);
  }
  public void GetObjectData(SerializationInfo info, StreamingContext context) {
   throw new NotImplementedException();
  }
 }
 #endregion
}

So ya, it's not exactly easy to implement. For those familiar with standard patterns, this is very similar to the singleton pattern, except there is more than one instance, and they are named as fields on the type. You can't directly instantiate a new expandable enum, thanks to the private constructor. You can only reference instances by the fields given, (Foo, Bar, and Zap) or by using GetInstance, which either gives you a standard one, or creates a new one and stores that instance as well. In this example, i've used the Number property to determine if I need to make a new instance in GetInstance, but you can use whatever you want. The real interesting thing here, is the ISerializable stuff. You see, if I were to simply mark the type with the Serializable attribute, then the default implementation would make it possible that an instance with Foo and 1 as the properties was not .Equal to the .Foo instance built into the type. So I implement ISerializable with a proxy IObjectReference. On serialization, I actually serialize the proxy class, which will return the correct instance of MyExpandableEnum via GetRealObject, which is called by the serialization framework for me.

The one thing I am not sure of, is the derivation from MarshalByRefObject. I believe that this will let MyExtendableEnum.Foo in one appdomain be .Equal with MyExtendableEnum.Foo in another AppDomain, but I am not sure. At any rate this is the reasoning behind that decision.

Overall, I think this is an effective way of having a type with pre-determined values, but still be extendable at runtime.

Comments

 

bopel said:

andy rox.
July 15, 2003 6:51 AM

Leave a Comment

(required)  
(optional)
(required)  
Add