Skip to content

App user settings

Suppose you're making an extension-app that defines a Translate Excel function that uses the Google Translate API to translate text from one language to another.

Google's Translate API requires an API key to work. If you're only using your extension in-house, you could embed the API key inside the source code of your app. However, if you're publishing your app and you don't know who is going to be using it, you wouldn't want to include your own API key in the app, since users might run up a sizable bill on your account.

Ideally, you'd want the end user to supply their own API key instead, so you don't have to worry how much people are using it. This poses a question: where does the user enter their API key?

You could have the api key be an argument of the Translate function. But this would be extremely inconvenient for the user because they would need to enter the (long and un-rememberable) API key every time they want to use Translate function. Worse still, the api key would then be saved inside the workbook so sharing the workbook would also share the (secret) API key.

The proper way to solve this problem is for the extension to allow the user to configure it. After installing the extension app, the user would configure it by entering their Google API key, which would be stored on the user's machine rather than inside the workbook. Furthermore, the user would only need to enter their API key in one place, instead of every time they use the Translate function.

For this reason, each extension can define its own configuration UI.

Configuring extensions

The general approach

Handling settings involves the following four tasks:

  1. Defining a settings class
  2. Instantiating the settings object and registering it into the IOC container so other classes in the app can reference it
  3. Defining a UI for editing the settings object
  4. Ensuring the settings are persisted between sessions

The App base class (ApplicationModule) provides default implementations for tasks 2, 3 and 4, so in most cases you just need to define a class that will represent your settings.

Defining a settings class

To scaffold a settings class, use the "Add->Settings class" command in the context menu:

Add settings class

This will scaffold a simple class that looks something like this:

1
2
3
4
5
6
7
8
public class Settings1 : ISettings
{
    [Trackable, Editable]
    public int MyProperty1 { get; set; } = 10;

    [Trackable, Editable]
    public string MyProperty2 { get; set; } = "Sample text";
}

Compiling the code as-is gives the following UI in the "Configure extensions" dialog:

Add settings class

Important things to note about the generated class are:

  • It implements the ISettings interface
  • Properties have the [Trackable] attribute
  • Properties have the [Editable] attribute

The ISettings interface is a marker interface without any members. It's used to mark a class as the settings class. On startup, the App object will scan the assembly looking for a class that implements this interface. If it finds one, it will instantiate it, register it with the container, and set up persistence for it. Scanning the project and instantiating the settings class is done by the virtual CreateSettingsObject method which can be overriden in case different behavior is needed.

The [Trackable] attribute marks a property for persistance. Only properties marked with this attribute are persisted between sessions.

The Jot library is used for persistence.

Marking a property with the [Editable] attribute will make it show up in the property editor. The property editor is used as the default control for editing the configuration object, though users are free to supply their own UI.

Customizing the UI

The default UI for editing the configuration object is a property editor control. This is fine for most purposes and various customizations can be done using attributes.

If you would like to use an entirely different custom control for editing the configuration, you can override the GetConfigUI method in your App class and provide it.

For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class App : ApplicationModule
{
    public App(IAppHost appHost)
        : base(appHost)
    {
    }

    protected override ConfigUI GetConfigUI(object settingsObject)
    {
        var rootControl = new StackPanel() 
        { 
            DataContext = settingsObject, 
            Orientation = Orientation.Vertical 
        };

        var label = new Label();
        label.SetBinding(Label.ContentProperty, nameof(Settings1.MyProperty1));

        var btn = new Button(){ Content = "I'm a button"};
        btn.Click += (s, e) => MessageBox.Show("button clicked");

        rootControl.Children.Add(label);
        rootControl.Children.Add(btn);

        return new ConfigUI(
            rootControl, 
            () => { MessageBox.Show("Configuration updated!"); });
    }
}

The above code would result in the following UI: Custom editor

The GetConfigUI method accepts the settings object and returns a ConfigUI instance. The ConfigUI instance contains a reference to the WPF control that will be displayed in the "Configure extensions" dialog and, optionally, an action that should be executed when the user clicks "OK" in the dialog.

Simple user interfaces can be defined through code, but since QueryStorm doesn't have a WPF/XAML designer, more complex UIs can be tricky to define. In case a more complex UI is needed, it's best to implement it in Visual Studio, and then either copy the code from there into QueryStorm, or define the UI as a separate dll in Visual Studio and reference it from the QueryStorm project.