Rico Suter's blog.
 


Often, we face the problem that our customers need highly configurable applications but there is no time to develop rich configuration GUIs. This article describes a possible solution which is a compromise between editing raw configuration files and implementing rich configuration GUIs.

The idea: First, implement some simple POCO classes (Plain Old CLR Object) representing the structure of the required configuration. Using a JSON serializer these classes can then be written and read from a JSON configuration file. Additionally, store an auto-generated JSON schema file alongside the configuration file. To edit the configuration file after its creation, use the tool Visual JSON Editor which automatically generates a GUI from the given JSON schema.

Let’s have a closer look at how this works.

Used components and applications

The following components and applications are used in this article:

  • Json.NET library: Excellent library which provides JSON serialization methods and classes.
  • NJsonSchema: Library to generate JSON Schemas from .NET classes
  • Visual JSON Editor: A JSON editor which dynamically generates a GUI based on a JSON schema file. You can use this application to view and edit the resulting configuration file.

Both components are open-source and free to use.

Implement configuration classes

First, implement some configuration classes (POCOs):

public class ApplicationConfiguration
{
    public ApplicationConfiguration()
    {
        Language = "en"; 
        ConnectionTimeout = TimeSpan.FromSeconds(5);
        Plugins = new List<PluginConfiguration>();
    }

    /// <summary>The user interface language of the application.</summary>
    public string Language { get; set; }
    public TimeSpan ConnectionTimeout { get; set; }
    public List<PluginConfiguration> Plugins { get; set; }
}

public class PluginConfiguration
{
    public PluginConfiguration()
    {
        IsActive = true;
        Path = ""; 
    }

    public bool IsActive { get; set; }

    [JsonProperty(PropertyName = "Path"]
    public string PathInFilesystem { get; set; }
}

As you can see, the constructors contain the code to initialize the classes with their default values. This is needed to generate the default configuration file which is shown later.

Using the JsonSchemaGenerator class you can generate the following JSON schema from these classes:

{
  "type": "object",
  "properties": {
    "Language": {
      "required": true,
      "description": "The user interface language of the application. ", 
      "type": [
        "string",
        "null"
      ]
    },
    "ConnectionTimeout": {
      "required": true,
      "type": "string"
    },
    "Plugins": {
      "required": true,
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "IsActive": {
            "required": true,
            "type": "boolean"
          },
          "Path": {
            "required": true,
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    }
  }
}

Load and save the JSON configuration file

Now, you can use the configuration classes together with the Json.NET JSON serializer to load and save a configuration file. In most cases, the configuration is read at application start and saved on exit. If you develop a WPF application, you can override the OnStartup and the OnExit method of the App class:

public partial class App
{
    public static ApplicationConfiguration Configuration { get; private set; }

    protected override void OnStartup(StartupEventArgs e)
    {
        Configuration = JsonApplicationConfiguration
            .Load<ApplicationConfiguration>("Config");

        base.OnStartup(e);
    }

    protected override void OnExit(ExitEventArgs e)
    {
        JsonApplicationConfiguration.Save("Config", Configuration);
        base.OnExit(e);
    }

...

A simplified version of the JsonApplicationConfiguration class looks as follows:

public static class JsonApplicationConfiguration
{
    private const string ConfigExtension = ".json";
    private const string SchemaExtension = ".schema.json";

    public static T Load<T>(string fileNameWithoutExtension) 
        where T : new()
    {
        var configPath = fileNameWithoutExtension + ConfigExtension;

        CreateSchemaFile<T>(fileNameWithoutExtension);

        if (!File.Exists(configPath))
            return CreateDefaultConfigurationFile<T>(fileNameWithoutExtension);

        string content = File.ReadAllText(configPath, Encoding.UTF8);
        return JsonConvert.DeserializeObject<T>(content);
    }

    public static void Save<T>(string fileNameWithoutExtension, T configuration)
        where T : new()
    {
        CreateSchemaFile<T>(fileNameWithoutExtension);

        var configPath = fileNameWithoutExtension + ConfigExtension;
        string content = JsonConvert.SerializeObject(configuration);

        File.WriteAllText(configPath, content, Encoding.UTF8);
    }

    private static T CreateDefaultConfigurationFile<T>(string fileNameWithoutExtension) 
        where T : new()
    {
        var config = new T();
        var configData = JsonConvert.SerializeObject(config);
        var configPath = fileNameWithoutExtension + ConfigExtension;

        File.WriteAllText(configPath, configData, Encoding.UTF8);

        return config;
    }

    private static void CreateSchemaFile<T>(string fileNameWithoutExtension) 
        where T : new()
    {
        var schemaPath = fileNameWithoutExtension + SchemaExtension;
        var schema = Task.Run(async () => await JsonSchema4.FromTypeAsync<T>()).GetAwaiter().GetResult();

        File.WriteAllText(schemaPath, schema.ToJson(), Encoding.UTF8);
    }
}

As you can see, the JSON schema file gets automatically created or overwritten when the application starts. This ensures that the schema always reflects the expected configuration structure of the current application version. If there is no configuration found, a default configuration file is created.

The full version of the class can be found here - this class also supports storing the configuration in the AppData directory:

Edit the configuration file

After starting the sample application, the files Config.json and Config.schema.json should be created in the application directory. The Config.json file can now be manually edited with a text editor of choice. The trick here is to open the file using the Visual JSON Editor application which takes the JSON schema file and generates a nice looking GUI which is completely tailored to edit the given configuration structure:

You can download the sample JSON files here, just extract the files and open the SampleConfig.json file.

Final words

Some features of the JSON schema specification are currently not implemented in the Visual JSON Editor application, for example type inheritance. However, for most scenarios the application should provide a good service and an improvement of the application is always possible as it is open-source and accepts pull requests.

Update 2/22/2016: Visual JSON Editor now uses NJsonSchema for JSON Schema reading and validation. This way the problems with Json.NET are solved…



Discussion