App Management

Overview

The .NET App Management API is accessible through io.AppManager.

Listing Apps

To get a collection of all available interop-enabled apps, use the AwaitApplications() method:

var apps = await io.AppManager.AwaitApplications();

To get a specific app, use AwaitApplication() and pass a predicate function for finding the desired app:

var app = await io.AppManager.AwaitApplication(a => a.Name == "appName");

See the .NET App Management example on GitHub.

Starting Apps

To start an app, use the Start() method of an app instance:

// Get the app.
var app = await io.AppManager.AwaitApplication(a => a.Name == "appName");

// Start the app.
app.Start();

App Context

The Start() method accepts a context object (app starting context) as an optional argument. The following example demonstrates how to create an app context and pass it to the app you want to start:

// Create app context.
var context = AppManagerContext.CreateNew();
context.Set("selectedClient", "3");

// Pass the context when starting the app.
app.Start(context);

App Instances

Each interop-enabled app may have multiple running instances. Each instance of a .NET app may have multiple windows belonging to that particular instance.

Current Instance

To get a reference to the current instance of your app, use the MyInstance property:

var myAppInstance = io.AppManager.MyInstance;

Instance Windows

To get a collection of all io.Connect Windows belonging to an app instance, use the Windows property of the app instance:

// Get an app.
var app = await AppManager.AwaitApplication(a => a.Name == "appName");
// Get the windows belonging to the app instance.
var appWindows = app.Instances.FirstOrDefault()?.Windows;

Events

The .NET App Management API offers methods for monitoring app and instance events - adding/removing apps, starting/stopping instances and more.

App

To get notified when an app definition has been added, use the ApplicationAdded event:

io.AppManager.ApplicationAdded += (appManager, appArgs) =>
    {
        var appName = appArgs.Application.Name;
    };

To get notified when an app definition has been removed, use the ApplicationRemoved event:

io.AppManager.ApplicationRemoved += (appManager, appArgs) =>
    {
        var appName = appArgs.Application.Name;
    };

To get notified when an app definition has been updated, use the ApplicationUpdated event:

io.AppManager.ApplicationUpdated += (appManager, appArgs) =>
    {
        var appType = appArgs.Applicaton.ApplicationType;
    };

Instance

To get notified when an app instance has been started, use the ApplicationInstanceStarted event:

io.AppManager.ApplicationInstanceStarted += (appManager, instanceArgs) =>
    {
        var instanceId = instanceArgs.Instance.Id;
    };

To get notified when an app instance has been stopped, use the ApplicationInstanceStopped event:

io.AppManager.ApplicationInstanceStopped += (appManager, instanceArgs) =>
    {
        var instanceId = instanceArgs.Instance.Id;
    };

Multi Window Apps

io.Connect .NET offers support for apps consisting of multiple windows. Each child window can be registered as an io.Connect app that you can save and restore in a Layout, start directly from the io.Connect launcher, and more. This feature allows you to register WPF and WinForms windows at runtime and to properly persist their state when saving and restoring a Layout.

See the .NET Multi Window Demo example on GitHub.

Registering Child Windows

To register a child window as an app, use the RegisterAppFactory() method:

io.AppManager.RegisterAppFactory(builder =>
    {
        builder
            .WithName(AppName)
            .WithTitle(AppTitle)
            // Passing the Main Window as context.
            .WithContext(this)
            // Registering the app as a tab window.
            .WithType(GlueWindowType.Tab);
    }
);

To register a WPF or a WinForms child window as an app, you can also use the RegisterWPFApp() or the RegisterWinFormsApp() generic methods respectively:

io.AppManager.RegisterWPFApp(builder =>
    {
        builder
            .WithName(AppName)
            .WithTitle(AppTitle)
            // Passing the Main Window as context.
            .WithContext(this)
             // Registering the app as a tab window.
            .WithType(GlueWindowType.Tab);
    }
);

The registered app window should implement the IGlueApp interface where you can specify the types of the state to save and restore, and the execution context. The IGlueApp interface has the following methods:

  • GetState() - invoked when io.Connect Desktop requests the current state of the window (e.g., when the window is being saved in a Layout).

Implementing the GetState() method:

public async GetState()
{
    // Returning the state that will be saved when the window is saved in a Layout.
    return Dispatcher.Invoke(() =>
    {
        // `SymbolState` is the class used for the state of the window.
        var state = new SymbolState()
        {
            ActiveSymbol = Symbol.Text
        };

        return state;
    });
}
  • Initialize() - invoked to restore the state when the window is started by io.Connect Desktop. Accepts the execution context, the app state to restore, the io.Connect API object, the starting context for the app and the io.Connect Window object.

Implementing the Initialize() method:

public void Initialize(MainWindow context, SymbolState state, Glue42 io, GDStartingContext startingContext, IGlueWindow myWindow)
{
    // Invoked when the window is restored or opened by the platform.
    Dispatcher.Invoke(() =>
    {
        Symbol.Text = state?.ActiveSymbol ?? DefaultSymbol;
    });
}
  • Shutdown() - Invoked when the app is closed by io.Connect Desktop.

Implementing the Shutdown() method:

public void Shutdown()
{
    // Here you can implement your own graceful cleanup of native resources, connections, etc., for this child app.
    // After that, close the window. Otherwise, the platform will wait for it to timeout and then will force close it.
    Close();
}

Saving & Restoring State

io.Connect apps can participate in Layouts. When an app is saved as part of a Layout, it can be started in a specific state when the Layout is restored. To specify the app state to be persisted, use the SetSaveRestoreStateEndpoint() method of the InitializeOptions object when initializing the io.Connect library in your main window:

// In your main window.

var options = new InitializeOptions();

options.SetSaveRestoreStateEndpoint(_ =>
    // Return the state to be saved when the app is saved in a Layout.
    new AppState
    {
        SelectedIndex = Selector.SelectedIndex
    }.AsCompletedTask(), null, Dispatcher
);

Glue42 io = await Glue42.InitializeGlue(options);

To use the state when the window is restored, use the GetRestoreState() method of the io.Connect API object:

var appState = io.GetRestoreState();

Selector.SelectedIndex = appState?.SelectedIndex ?? -1;

Bootstrapped Apps

A common use-case is to have a bootstrapping app that handles user login and launches your apps. The bootstrapper can be any app and in the general case it isn't interop-enabled, but the apps it launches are. This scenario can be handled both through app definition or runtime configuration when initializing io.Connect in the respective apps.

⚠️ Note that defining app definitions for interop-enabled .NET apps isn't mandatory. Interop-enabled .NET apps announce themselves to io.Connect Desktop automatically when started. The configuration file is necessary only if you want your app to be launchable from io.Connect Desktop. If you decide to create app definitions for the bootstrapper and the bootstrapped apps, the name of the bootstrapper specified in the "name" property of the bootstrapper configuration, in the "launcherApp" property of the bootstrapped app definition, and in the initialization options in the bootstrapped app code must be the same.

You must specify the name of the bootstrapping app and clear the starting context passed by io.Connect Desktop to it. Clearing the starting context is very important, because io.Connect Desktop by default will store the command line arguments with which the bootstrapper has been started and will try to pass them again on restore, which in some cases may not be possible or may lead to undesired results.

App Definition

To specify the name of the bootstrapper in the app definitions of the bootstrapped apps, use the "launcherApp" property of the "details" top-level key:

// Configuration file for the bootstrapped app.
{
    "name": "BootstrappedApp",
    "type": "exe",
    "details": {
        "path": "%GDDIR%/PathToBootstrappedApp/",
        // Must match the app name specified in the
        // bootstrapper configuration and in the bootstrapped app code.
        "launcherApp": "MyBootstrapper"
    }
}

To clear the starting context for the bootstrapper, set the "startingContextMode" property of the "details" top-level key to "none". The following example configuration demonstrates how to instruct io.Connect Desktop to:

  • prevent saving the bounds of the bootstrapper;
  • exclude the bootstrapper from participating in Layouts;
  • prevent starting multiple instances of the bootstrapper;
  • run the bootstrapper as a hidden window;
  • clear the starting context for the bootstrapper;
  • track the bootstrapper using its process, because it isn't interop-enabled;
  • prevent closing the bootstrapper on shutdown;
// Example configuration file for the bootstrapper.
{
    "title": "My Bootstrapper",
    "type": "exe",
    // Must match the name specified in the `"launcherApp"` property of the
    // bootstrapped app definition and in the bootstrapped app code.
    "name": "MyBootstrapper",
    "ignoreSavedLayout": true,
    "ignoreFromLayouts": true,
    "allowMultiple": false,
    "hidden": true,
    "details": {
        "path": "C:/PathToBootstrapper/",
        "command": "MyBootstrapper.exe",
        "parameters": "",
        "startingContextMode": "none",
        "trackingType": "Process",
        "terminateOnShutdown": false,
    }
}

Runtime Configuration

It is mandatory to specify the name of the bootstrapper in the InitializeOptions object during the initialization of io.Connect in your bootstrapped app, irrespective of whether you have created an app definition for it. This is necessary, because when an interop-enabled .NET app announces itself to io.Connect Desktop, it overwrites the configuration settings specified in its configuration file.

The following example demonstrates how to instruct io.Connect Desktop to:

  • prevent closing the bootstrapped app on shutdown;
  • prevent passing previously saved by io.Connect Desktop startup arguments to the bootstrapped app on restore;
  • exclude the bootstrapped app from participating in Layouts;
  • prevent starting multiple instances of the bootstrapped app;
// Must match the name specified in the configuration of the bootstrapper
// and in the `"launcherApp"` property of the bootstrapped app definition.
const string LauncherApp = "MyBootstrapper";

var initializeOptions = new InitializeOptions
{
    // Must match the name specified in the bootstrapped app definition, if any.
    ApplicationName = "BootstrappedApp",
    AppDefinition = new AppDefinition
    {
        Title = "My Bootstrapped App",
        TerminateOnShutdown = false,
        LauncherApp = LauncherApp,
        StartupArguments = "",
        IgnoreFromLayouts = true,
        AllowMultiple = false
    }
};

Optionally, you can also register the bootstrapper at runtime. The registration is executed by the bootstrapped app, because the bootstrapper isn't interop-enabled:

// Must match the name specified in the configuration of the bootstrapper
// and in the `"launcherApp"` property of the bootstrapped app definition.
const string LauncherApp = "MyBootstrapper";

private static async Task RegisterBootstrapper(Glue42 glue, string bootstrapperLocation, string bootstrapperFile)
{
    var tcs = new TaskCompletionSource<(MethodInvocationStatus, string)>(TaskCreationOptions
        .RunContinuationsAsynchronously);

    io.GetService<IApplicationRegistryService>().RegisterHostApplication(new AppConfig
    {
        Name = LauncherApp,
        Type = "exe",
        Details = new AppConfigDetails
        {
            // Clearing the starting context for the bootstrapper in order to prevent
            // io.Connect Desktop from passing startup arguments to it on restore.
            StartingContextMode = StartingContextMode.None,
            // Track the bootstrapper through its process, because it isn't interop-enabled.
            TrackingType = TrackingType.Process,
            Command = bootstrapperFile,
            Path = bootstrapperLocation,
            // Prevent closing the bootstrapper on shutdown.
            TerminateOnShutdown = false
        },
        // Run the bootstrapper in a hidden io.Connect Window.
        Hidden = true,
        // Prevent starting multiple instances of the bootstrapper.
        AllowMultiple = false,
        // Don't auto start the bootstrapper when io.Connect Desktop starts.
        AutoStart = false,
        // Exclude the bootstrapper from participating in Layouts.
        IgnoreFromLayouts = true,
        //Don't save the bootstrapper bounds.
        IgnoreSavedLayout = true
    }, AppDefinitionLifetime.Default, r => tcs.TrySetResult((r.Status, r.ResultMessage)));

    if (await tcs.Task.ConfigureAwait(false) is var status && status.Item1 != MethodInvocationStatus.Succeeded)
    {
        throw new Exception(status.Item2);
    }
}