Custom Data Types

In some cases you may want to have additional support for custom data types in the options engine. This does not need to be one of the built-in .NET types; you can also add support for types that you created yourself, and that are only meaningful to your particular application. If you e.g. put a custom type in a separate group by using the attribute configuration, you can de facto add whole extra pivot items to the options page that you can design in any way you like. I strongly recommend to take a look at the included "Extras" project which also uses this mechanism to add support for additional data types.

The IOptionsFactoryExtender interface

To extend the existing collection of supported data types, YLOD offers an interface called IOptionsFactoryExtender which only has two methods:

/// <summary>
/// Describes an extender to the existing <see cref="OptionsFactory"/>.
/// </summary>
public interface IOptionsFactoryExtender
{
    /// <summary>
    /// Determines the types that should be used for a given property.
    /// The extender should set types only if it is capable of handling the type of property.
    /// If the property should be handled by the existing <see cref="OptionsFactory"/>, the extender
    /// is supposed to simply leave the passed in type references as-is.
    /// </summary>
    /// <param name="property">The property the types need to be determined for.</param>
    /// <param name="optionType">Type of the option to use for the given property.</param>
    /// <param name="optionAttributeType">Type of the option attribute to use for the given property.</param>
    void DetermineTypesFor(PropertyInfo property, ref Type optionType, ref Type optionAttributeType);

    /// <summary>
    /// Sets the custom values of an option.
    /// </summary>
    /// <param name="property">The property the option was created for.</param>
    /// <param name="option">The option that needs its custom values to be set.</param>
    /// <param name="optionAttribute">The option attribute that was applied to the property, if available.</param>
    /// <param name="instance">The instance the property value can be found on.</param>
    void SetCustomValues(PropertyInfo property, Option option, OptionAttribute optionAttribute, object instance);
}

To understand this interface, you have to understand the way the engine works:
  • When you pass in an arbitrary object, the properties of that object are inspected using reflection.
  • For each property, the OptionAttribute is retrieved (if applicable).
  • A factory named OptionsFactory uses the property and attribute information to create an appropriate Option-derived instance for the property.
  • In a second step, a view factory derived from IOptionsViewFactory creates views (editor controls) for each of the Option-derived objects.

When you want to provide new or changed views, you simply implement the view factory interface and configure the OptionsService to use your new view factory (see the separate documentation on the view factory for more details). For the OptionsFactory however, I chose a slightly different way for extension. Instead of replacing that factory with your own implementation, the engine offers hooks into the existing implementation of the factory by implementing the above interface. The reason for this different approach is that unlike the default view factory which only contains very little logic, the OptionsFactory has a lot of relatively complicated things in it regarding reflection and generating proper Option objects from properties and attributes. If I had enabled providing your own implementation of that factory you would have to duplicate most of this complex logic, or I would have had to make public most of it. Instead, I went the route of providing this extender interface. Here is how it works.

DetermineTypesFor()

This method is called by the OptionsFactory when the derived type of Option implementation as well as the suitable derived OptionAttribute type need to be determined for a property that was discovered on an object. As an example, take a look at the implementation in the ExtrasOptionsFactoryExtender of the "Extras" project:

public virtual void DetermineTypesFor(PropertyInfo property, ref Type optionType, ref Type optionAttributeType)
{
    if (property.PropertyType == typeof(TimeSpan))
    {
        optionType = typeof(TimeSpanOption);
        optionAttributeType = typeof(OptionTimeSpanAttribute);
    }
    else if (property.PropertyType == typeof(Color))
    {
        optionType = typeof(ColorOption);
        optionAttributeType = typeof(OptionColorAttribute);
    }
}

As you can see, the logic here is very simple: for each of the supported data types the extender sets the correct Option and OptionAttribute type that should be used. For all types not supported by the extender, no action is performed, which means the base implementation of the OptionsFactory will try to take care of it.

SetCustomValues()

After the OptionsFactory has created the proper Options-derived instance, retrieved the attribute values (if applicable) and initialized and set all basic properties, it calls the SetCustomValues method of the extender, passing in all available and created data. This is the place where the extender can set additional property values on the Options-derived type that are unknown to the OptionsFactory base implementation. In the case of the "Extras" project, the extender does this:

public virtual void SetCustomValues(PropertyInfo property, Option option, OptionAttribute optionAttribute, object instance)
{
    if (option is TimeSpanOption)
    {
        SetCustomTimeSpanValue(option as TimeSpanOption, optionAttribute as OptionTimeSpanAttribute);
    }

    // no need to set custom values for the color option, because it has none
}

private void SetCustomTimeSpanValue(TimeSpanOption option, OptionTimeSpanAttribute attribute)
{
    if (attribute != null)
    {
        // set the maximum and step
        option.Maximum = ConvertFromString(attribute.Maximum, TimeSpan.MaxValue);
        option.Step = ConvertFromString(attribute.Step, TimeSpan.FromSeconds(1.0));
    }
}

Again the logic is not very complex here. If a TimeSpanOption is passed in, the additionally configurable parameters for such an option are parsed and transferred from the attribute to the Option instance.

Additional Steps

To extend the functionality in the above described way, the following steps of course also need to be performed:
  • The attributes to decorate properties of the newly supported type need to be implemented. That means: if you want to support a new type XYZ, you have to provide an attribute derived from OptionAttribute that contains all additional parameters that can be configured for an option of type XYZ. In the above example, this is e.g. the OptionTimeSpanAttribute.
  • The Option-derived implementation for the new type must be provided. In the above example this is e.g. the TimeSpanOption implementation. These implementations have the same (maybe processed) properties like their corresponding attributes, plus additional validation logic or similar things, depending on the requirements.
  • If you provide an extender, it is also implied that you provide a custom view factory (see the separate documentation on the view factory), because in the second step of the initially described creation process the engine will query the view factory for an editor for your newly supported type. If you do not provide such an editor, the options page cannot display the property value to the user for editing.

Again, please take a look at the "Extras" project, which performs all the required steps to extend the base functionality by some additional data types. Following this sample gives you an idea of what exactly is required to make your extensions work as intended.

Using the Extender

As with the view factory, you only need to tell the OptionsService that it should use the extender, before you call the Show method on it to navigate to the options page.

OptionsService.Current.OptionsViewFactory = new MyOptionsViewFactory();
OptionsService.Current.OptionsFactoryExtender = new MyOptionsFactoryExtender();

Please note that you need to repeat the factory and extender configuration above when the user returns to the application after a tombstoning situation; for more details, see the documentation section on tombstoning.

Last edited Nov 20, 2011 at 4:47 PM by Mister_Goodcat, version 1

Comments

No comments yet.