# Fluent configuration

When you use SelectedObject to fill your PropertyGrid, you can either use attributes attached to properties to customize how they look and feel, hook into the grid's events, or use the new fluent API. In the first case, you need to have access to the source code of your target instance, which is not always possible. If not, you could so far handle some events but it meant first checking the identity of the property involved (i.e. possibly a lot of if/else when you have many properties to configure). There is now a way to do the same in custom helper classes that are external to your target instances, and this is done with a fluent API.

This whole API is a proof of concept that, at least, proves that SPG was flexible enough to develop such an extension. It does not contain every possible method that will allow you to customize a property like you did with the 2 other options. But fortunately, there is a way to add your own extensions to this API. You can also let me know what is missing and is important to you and I will add it to the API.

# Adding properties configurations

In a central place of your app, before setting a target instance to the PropertyGrid, add this code:

FluentSelectedObjectConfigurator.CreateConfiguration(config =>
{
    // 1. config.AddTargetConfiguration<TargetClass>()
    //       .ForProperty(...

    // 2. config.AddConfigurationProfile<TargetClassProfile>();
    //  or config.AddConfigurationProfile(typeof(TargetClassProfile));

    // 3. config.AddConfigurationProfiles();
})
.Build(myPropertyGrid);

myPropertyGrid.SelectedObject = new TargetClass();

The 3 options you see in the code represent how you can add sets of properties configurations of your classes that you assign to SelectedObject. In each case, you act on a ISelectedObjectConfiguration instance (config).

Once all your possible configurations have been set, call the Build method and pass your PropertyGrid as argument. This internally browses all your customizations and hooks into the PropertyGrid's events system to configure the properties at the right time.

# AddTargetConfiguration

If the number of your customizations is relatively small (for testing for instance), you can do them right in the CreateConfiguration body. Obviously, if you have too many target classes with many properties to customize, this would produce some too long and linear code.

To do this, call AddTargetConfiguration on the config instance, passing your target class as the generic parameter. From there, configure each property on the ITargetConfiguration<TargetClass> that is returned to you.

# AddConfigurationProfile

The best way to configure some properties of a specific target class is by creating a separate class called a profile. This keeps your code clean and adheres the separation of concerns principle. You do so by deriving from the SelectedObjectProfile class:

public class TargetClassProfile : SelectedObjectProfile
{
    public TargetClassProfile()
    {
        AddTargetConfiguration<TargetClass>()
            .ForProperty(...
    }
}

In its constructor, call AddTargetConfiguration like in the previous section and customize your properties from there.

As seen in the intro, to add this profile class to the global configuration, you have 2 ways, depending on your style:

config.AddConfigurationProfile<TargetClassProfile>();
// or
config.AddConfigurationProfile(typeof(TargetClassProfile));

# AddConfigurationProfiles

If you write a set of profiles, it is possible to register them in one call:

config.AddConfigurationProfiles();

Without parameters, this will search for profiles in the calling assembly. Alternatively, you can pass the assemblies you want to check:

config.AddConfigurationProfiles(assembly1, assembly2);

# Customizing the grid itself

You may want to customize the grid when a specific target class is assigned to it. For instance, let's say you have a target class with a lot of long comments to display in the grid:

AddTargetConfiguration<TargetClass>()
    .OnSelectedObjectChanged(grid => {
        grid.CommentsVisibility = true;
        grid.CommentsHeight = 150;
    })

Note that you also have a OnSelectedObjectChanging method to react before SelectedObject is actually assigned to.

# Customizing properties

You configure a specific property of your target class by calling the ForProperty method on the ITargetConfiguration instance you received by calling AddTargetConfiguration:

AddTargetConfiguration<TargetClass>()
    .ForProperty(x => x.Color, config => config
        .Id(1000)
        .Feel(PropertyGrid.FeelNone)
        .Look<PropertyColorLook>(true)
        .PropertyChanged(x => x.Valid, (args, grid) => {
            // Do stuff
        })
    )

The first argument of ForProperty is an expression so that there is no hardcoded string and intellisense can help you. The second argument gives you an object letting you configure various elements of your property. So far, you can call these methods:

  • DisplayName: to assign a readable display name (left column of the PropertyGrid).
  • Comment: to assign a description.
  • Id: to assign a custom hardcoded ID.
  • Sorted: to order the properties inside their category.
  • Feel: to assign a feel.
  • ChildFeel: the same for a child property.
  • Look: to assign a look.
  • ChildLook: the same for a child property.
  • DropDownContent: to assign the control used in the listbox part of a list feel. See PropertyDropDownContentAttribute.
  • ChildDropDownContent: the same for a child property.
  • UITypeEditor: to assign a UITypeEditor.
  • TypeConverter: to assign a TypeConverter.
  • Validator: to assign a PropertyValidator.
  • ChildValidator: the same for a child property.
  • Category: to assign a category name and optionally set other settings for the category.
  • DefaultProperty: to set a property as the selected one after SelectedObject has been called.
  • FilterOut: filters ou a property. See the PropertyFilterOut event.
  • DisplayedValues: to assign the possible choices a property's value can take. See the DisplayedValuesNeeded event.
  • ChildDisplayedValues: the same for a child property.
  • IsPassword: to assign password look and feel.
  • UseFeelCache: to use a cache for the displayed values of a list feel. See UseFeelCacheAttribute.
  • HideChildProperties: to prevent the creation of a set of specific child properties.
  • ShowAllChildProperties: to choose whether to show or hide all child properties.

Additionnaly, it's possible to react to some PropertyGrid's events, specifically for a property, which is quite cool. Handled events are:

# Customizing types

Sometimes, customizations on some properties of the same type may be repetitive. It's then practical to customize types rather than properties. Here is how to set up all boolean properties with a checkbox and no displayed strings:

AddTargetConfiguration<TargetClass>()
    .ForType<bool>(config => config
        .Look<PropertyCheckboxLook>()
        .Feel(PropertyGrid.FeelCheckbox)
        .DisplayedValuesNeeded(new string[] { "", "" })
)

For the moment, only Look, Feel and DisplayedValuesNeeded are available. Contact me if you need more.

# Customizing categories

Do you know the way to customize a row that represents a category in the PropertyGrid? In MSPG there is no way. In SPG, you can handle the PropertyCreated event, check that you have the right category and then act on it if this is the one.

Here is now where the fluent API is so practical:

AddTargetConfiguration<TargetClass>()
    .ForCategory("Misc", category =>
        category.Comment = "This is the famous miscellaneous category."
    )

# Categories and subproperties

As demonstrated before, ForProperty gives you an object on which you can call Category to define the parent category of a property. And ForCategory lets you define some settings of categories. It can be even more intuitive than that: properties can be defined inside a category definition, like so:

AddTargetConfiguration<TargetClass>()
    .ForCategory("Misc", config => config
        .Comment("This is the famous miscellaneous category.")
        .Sorted(1)

        .ForProperty(x => x.AutoStart, ......)
    )

Contrarily to the previous section, this ForCategory method override does not give your a Property instance on which you can directly act. It gives you a configuration instance for calling OnProperty on it. Right now, it has methods for setting:

  • Comment: a comment displayed on the bottom of the PropertyGrid,
  • Sorted: an index to sort the category among its siblings,
  • ValueText: a text displayed in the value column of the PropertyGrid,
  • ImageIndex: an index to an image inside a global ImageList set to the PropertyGrid.

Note

There is no way to refer to a category other than by its name, unfortunately.

# Handling non implemented customizations

The fluent API object design was made so that you can add some (maybe not all types of) unimplemented customizations.

You first have to write an extension method for the TargetMemberConfiguration<TargetType, TMember>> class. It will return itself. Add the parameters you need in the method signature. Its job is to register the "action" of type TargetMemberAction that will actually customize the property. To do so, it has to call the AddTargetMemberAction method. Its parameter is a .Net Action where you can use the passed TargetMemberActions instance to register your customization with RegisterCustomPropertyAction. Enough talk! Let's assume that the api does not allow specifying the description of a property. Here is how to add the functionality outside of SPG:

public static class CommentExtension
{
    public static TargetMemberConfiguration<TargetType, TMember> Comment<TargetType, TMember>(
        this TargetMemberConfiguration<TargetType, TMember> config, string comment)
    {
        config.AddTargetMemberAction(tma => tma.SetComment(config.PropertyInfo, comment));

        return config;
    }

    public static void SetComment(this TargetMemberAction memberActions,
        MemberInfo memberInfo, string comment)
    {
        memberActions.RegisterCustomPropertyAction(
            PropertyGridEventType.PropertyCreated,
            memberInfo,
            new PropertyCommentAction(comment));
    }
}

RegisterCustomPropertyAction takes 3 arguments:

  1. A type of PropertyGrid event. Its handler will call your specific action.
  2. The .Net MemberInfo describing your property.
  3. An instance of your custom action.

To finish, let's define the PropertyCommentAction class that must implement IPropertyAction's RunAction method:

public class PropertyCommentAction : IPropertyAction
{
    private readonly string _comment;

    public PropertyCommentAction(string comment)
    {
        _comment = comment;
    }

    public void RunAction(Property property)
    {
        property.Comment = _comment;
    }
}
Last Updated: 5/25/2022, 1:18:09 PM