You would think I would be proud of my success yesterday and stopped there… and you would be wrong. Once I got the BooleanAnimationOptionAttribute done, I wanted to convert the rest of the options classes for the three animations to the new format. I created a RangedIntegerAnimationOptionAttribute which allows you to specify a default and a min/max which was useful, and then I created a StringCollectionAnimationOptionAttribute to allow me to load in a list of strings (for browser) and then subclasses that using a delegate to provide one StringCollection that would make sure they were valid Urls.
Now, the base of the screensaver class is built from the ground up to support multiple monitors, mostly so I can use it at work and see it if I have one of my monitors switched away to another system. There, delegates are used to communicate between the individual forms and the controller to make sure that if one stops, they all stop. That was pretty much piecing things together from scraps thanks to Google. This was pretty much copied and changed when I needed a way for the panels to talk to the configuration form and let it know that something in the panel had changed and we should show the apply/reset buttons. This time, I simply called it a ValidateSettingString and wired it into the XmlSettings class so that I could provide a validation method when loading the settings from the settings file. Worked pretty much off the bat.
Based on what I have seen so far, I like delegates. Essentially they are interfaces at the method level, and they define a contract that you can pass into a specific method. That is very useful in some cases. Also, creating anonymous methods to pass in as delegate parameters is occasionally useful, but I still prefer to pass in a normal method from a class to make sure I can check it out nicely and document it.
Up to this point, it was pretty easy and I had almost everything in the screensaver translated over to the new way, and it is slick. What was ugly before, declaration in one place, loading in another, and saving in a third, is now.
///
/// Size of the cells that we are representing.
///
[RangedIntegerAnimationOption(30, MinimumCellSize, MaximumCellSize)]
public int SizeOfCells { get; set; }
The nice part about this is that it keeps all of the pertinent information in one place. I placed the minimum and maximum for the ranged integers as public constants so the forms can pick up that information and use them for the numeric up/down controls. I have toyed with eliminating that and figuring out some way to grab the min/max from the declaration itself, but no dice so far.
Now, the final thing left was to handle the array of PerScreenOptions for the Swarm animation, as I had it set up to work per screen and each screen configurable separately. This is where the headaches began and the research started back up. Before, I had nice solid information about the types and default values and it was relatively easy. Now, I have to figure out how many items, store them, load them, and default them. The big piece: given an array of a given class, how do I figure out what item are in there and deal with the array itself.
The first part was easy: create a new NestedArrayAnimationOptionAttribute and assign it to the array I wanted to serialize to the settings file. In both my AutoLoadOptions and AutoSaveOptions, if I encounter that attribute, I need to go to a new method that is specifically going to be written to deal with arrays.
So dealing with the save first, I need to get a handle on the array to interrogate it: This proved simple:
// Figure out which array to deal with. Once we do that,
FieldInfo memberAsField = nextDiscovery.MemberAttachedTo as FieldInfo;
System.Array arrayToProcess = (System.Array)memberAsField.GetValue(this);
This was possible as all arrays in C# are descended from the System.Array class and it provides a useful manner in which to deal with the arrays disregarding type. After getting that, its a simple loop to go through each of the elements of the array to interrogate each one. System.Array has a nice GetValue method to let you grab the element at a given point in the array, and after that its pretty simple, reusing the exploration that we had in the first place, but providing the instance of the array element instead of the class in which the array is.
Now that we have a way of looking at all of the elements, just have to do some bookkeeping to update the elements in the settings file. After doing some quick experimentation, it was easier to remove all of the array elements than trying to get other permutations right, so the first thing in the method is to call the XmlSettings class to do just that. Then when we are going through each element in the array, make a call down to a new method in XmlSettings to make sure we have a node set up to contain the settings for that element. That required some reworking in the lower levels, but nothing really serious. After that, we pass the node for the element to the ApplySettings routines, and we are done. For all of that, we end up with a nicely organized array indexed class representation.
<Swarm>
<Item Name="MultipleDistinctScreens" Value="True" />
<Array Name="_screenOptions">
<Item Index="0">
<Item Name="SwarmsToShow" Value="3" />
<Item Name="InitialVelocity" Value="9" />
<Item Name="ColorCycleInSeconds" Value="1" />
<Item Name="BeeCount" Value="49" />
<Item Name="GlitterActive" Value="True" />
</Item>
</Array>
</Swarm>
Now on to the load. I knew this would be the hard one from the beginning. For the save, we had an established set of information that we wanted to persist. For the load, we have XML and we need to change it from data in the XML to fields and properties in the element of the array. This was bound to be at least a bit more difficult.
The first part was pretty easy… come up with an instance of the element we are trying to create:
// Figure out what the object we are creating is, and make sure we have a constructor for it.
String typeName = memberAsField.FieldType.FullName;
typeName = typeName.Substring(0, typeName.Length - 2);
Type arrayObjectType = Type.GetType(typeName);
Type[] noParameters = new Type[0];
ConstructorInfo arrayObjectConstructor = arrayObjectType.GetConstructor(noParameters);
We already have the information on the name of the item we want to load, and we take off the final [] to get the root instance. After that, a bit of magic to use reflection to get the object’s constructor. While it is true I could stand to have a bit more error checking around this, its okay for now.
List listOfObjects = new List();
Object nextItem = arrayObjectConstructor.Invoke(noParameters);
List listOfAttributes =
AttributeHelper.SearchForFieldsWithAttribute
(nextItem);
listOfAttributes.AddRange(AttributeHelper.searchForPropertiesWithAttribute
(nextItem));
Basically, create a list of objects that we will later put into our array and then create a sample object. We need this sample object as our routines for discovering our attributes rely on having an instance and its easier to do this than to rewrite for a type and maintaining two paths.
From here, its mostly the same as before. Given the list from above, we go through it and assign values from the XML into the objects one at a time in a loop. One difference is that we have a minimum number of objects we want to see in the array. Once we are done loading, we just fill up the array until it has the minimum number of objects using a DefaultValueFromAttribute method. This method is virtually identical to the one to fetch from the XML, minus the fetching from the XML. Once that is done, we create an array using System.Array.CreateInstance and dump all of the objects in our collection into the array and we are done.
Testing was pretty simple, and I am going to have to write some good unit tests around it, but they shouldn’t be too bad. The data translation is complicated in places, but pretty straightforward when you think about it from a big picture point of view. From a usability point of view, it makes the creation and maintenance of the options classes for the animations a lot easier and a lot less error prone.
Left to do now, add a bit of validation to make sure we are not applying a OptionAttribute to a type where it is obviously going to blow up. Also, good unit tests around the routine.