Creación de instancias de aplicaciones con la API del ciclo de vida de la aplicación

El modelo de creación de instancias de una aplicación determina si varias instancias del proceso de la aplicación se pueden ejecutar al mismo tiempo.

Requisitos previos

Para usar la API de ciclo de vida de la aplicación en el SDK de aplicaciones para Windows:

  1. Descargue e instale la versión experimental más reciente del SDK de Aplicaciones para Windows. Para más información, consulte Instalación de herramientas para el SDK de Aplicaciones para Windows.
  2. Siga las instrucciones para crear su primer proyecto de WinUI 3 o para usar el SDK de aplicaciones para Windows en un proyecto existente.

Aplicaciones de instancia única

Nota:

Para obtener un ejemplo de cómo implementar instancias únicas en una aplicación WinUI 3 con C#, consulte Creación de una instancia única de la aplicación en el blog para desarrolladores de Windows.

Las aplicaciones son de instancia única si solo puede haber un proceso principal en ejecución a la vez. Si se intenta iniciar una segunda instancia de una aplicación de instancia única, normalmente se activa la ventana principal de la primera instancia. Tenga en cuenta que esto solo se aplica al proceso principal. Las aplicaciones de instancia única pueden crear varios procesos en segundo plano y seguir considerándose de instancia única.

Las aplicaciones para UWP son de instancia única de forma predeterminada, pero tienen la capacidad de convertirse en aplicaciones de varias instancias al decidir en tiempo de inicio si se debe crear una instancia adicional o activar una instancia existente en su lugar.

La aplicación Correo de Windows es un buen ejemplo de una aplicación de instancia única. Al iniciar Correo por primera vez, se crea una nueva ventana. Si intenta iniciar Correo de nuevo, la ventana de Correo existente se activará en su lugar.

Aplicaciones de varias instancias

Las aplicaciones son de varias instancias si el proceso principal se puede ejecutar varias veces simultáneamente. Al intentar iniciar una segunda instancia de una aplicación de varias instancias, se crea un nuevo proceso y una ventana principal.

Tradicionalmente, las aplicaciones sin empaquetar son de varias instancias de forma predeterminada, pero pueden implementar instancias únicas cuando sea necesario. Normalmente, esto se hace con una sola exclusión mutua con nombre para indicar si ya se está ejecutando una aplicación.

El Bloc de notas es un buen ejemplo de una aplicación con varias instancias. Cada vez que intente iniciar el Bloc de notas, se creará una nueva instancia del Bloc de notas independientemente del número de instancias que ya estén en ejecución.

Diferencias entre la creación de instancias del SDK de aplicaciones para Windows y la creación de instancias de UWP

El comportamiento de creación de instancias en el SDK de aplicaciones para Windows se basa en el modelo y la clase de UWP, pero con algunas diferencias clave:

Clase AppInstance

Lista de instancias

  • UWP: GetInstances devuelve solo las instancias que la aplicación registró explícitamente para el posible redireccionamiento.
  • SDK de aplicaciones para Windows: GetInstances devuelve todas las instancias en ejecución de la aplicación que usan la API de AppInstance, tanto si han registrado una clave como si no. Esto puede incluir la instancia actual. Si desea que la instancia actual se incluya en la lista, llame a AppInstance.GetCurrent. Se mantienen listas independientes para distintas versiones de la misma aplicación, así como para instancias de aplicaciones iniciadas por distintos usuarios.

Registro de claves

Cada instancia de una aplicación de varias instancias puede registrar una clave arbitraria a través del método FindOrRegisterForKey. Las claves no tienen un significado inherente; las aplicaciones pueden utilizarlas de la forma o manera que deseen.

Una instancia de una aplicación puede establecer su clave en cualquier momento, pero solo se permite una clave para cada instancia; al establecer un nuevo valor, se sobrescribe el anterior.

Una instancia de una aplicación no puede establecer su clave en el mismo valor que otra instancia ya ha registrado. Si intenta registrar una clave existente, FindOrRegisterForKey devolverá la instancia de la aplicación que ya ha registrado esa clave.

  • UWP: una instancia debe registrar una clave para incluirse en la lista devuelta desde GetInstances.
  • SDK de aplicaciones para Windows: el registro de una clave se desacopla de la lista de instancias. Una instancia no necesita registrar una clave para incluirse en la lista.

Anulación del registro de claves

Una instancia de una aplicación puede anular el registro de su clave.

  • UWP: cuando una instancia anula el registro de su clave, deja de estar disponible para el redireccionamiento de activación y no se incluye en la lista de instancias devueltas desde GetInstances.
  • SDK de aplicaciones para Windows: una instancia que ha anulado el registro de su clave sigue estando disponible para el redireccionamiento de activación y todavía se incluye en la lista de instancias devueltas desde GetInstances.

Destinos de redireccionamiento de instancias

Varias instancias de una aplicación pueden activarse entre sí, un proceso denominado "redireccionamiento de activación". Por ejemplo, una aplicación podría implementar instancias únicas inicializándose solo si no se encuentra ninguna otra instancia de la aplicación en el inicio y, en su lugar, redirigir y salir si existe otra instancia. Las aplicaciones con varias instancias pueden redirigir las activaciones cuando corresponda según la lógica de negocios de esa aplicación. Cuando se redirige una activación a otra instancia, usa la devolución de llamada de Activated de esa instancia, la misma devolución de llamada que se usa en todos los demás escenarios de activación.

  • UWP: solo las instancias que han registrado una clave pueden ser un destino para el redireccionamiento.
  • SDK de aplicaciones para Windows: cualquier instancia puede ser un destino de redireccionamiento, independientemente de si tiene o no una clave registrada.

Comportamiento posterior al redireccionamiento

  • UWP: el redireccionamiento es una operación terminal; la aplicación finaliza después de redirigir la activación, incluso si se produjo un error en el redireccionamiento.

  • SDK de aplicaciones para Windows: en el SDK de aplicaciones para Windows, el redireccionamiento no es una operación terminal. Esto en parte refleja los posibles problemas en la terminación arbitraria de una aplicación Win32 que puede haber asignado memoria, pero también permite admitir escenarios de redireccionamiento más sofisticados. Supongamos una aplicación de varias instancias en la que una instancia recibe una solicitud de activación mientras realiza un trabajo que consume una gran cantidad de CPU. Esa aplicación puede redirigir la solicitud de activación a otra instancia y continuar su procesamiento. Este escenario no sería posible si la aplicación se cerrara después del redireccionamiento.

Una solicitud de activación se puede redirigir varias veces. La instancia A podría redirigir a la instancia B, que a su vez podría redirigir a la instancia C. Las aplicaciones del SDK de aplicaciones para Windows que aprovechan esta funcionalidad deben protegerse contra el redireccionamiento circular: si C redirige a A en el ejemplo anterior, existe un posible bucle de activación infinito. Depende de la aplicación determinar cómo controlar el redireccionamiento circular en función de lo que tenga sentido para los flujos de trabajo que admite la aplicación.

Eventos de activación

Para controlar la reactivación, la aplicación puede registrarse para un evento activado.

Ejemplos

Control de las activaciones

En este ejemplo se muestra cómo una aplicación se registra y controla un evento Activated. Cuando recibe un evento Activated, esta aplicación usa los argumentos del evento para determinar qué tipo de acción provocó la activación y responde correctamente.

int APIENTRY wWinMain(
    _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // Initialize the Windows App SDK framework package for unpackaged apps.
    HRESULT hr{ MddBootstrapInitialize(majorMinorVersion, versionTag, minVersion) };
    if (FAILED(hr))
    {
        OutputFormattedDebugString(
            L"Error 0x%X in MddBootstrapInitialize(0x%08X, %s, %hu.%hu.%hu.%hu)\n",
            hr, majorMinorVersion, versionTag, 
            minVersion.Major, minVersion.Minor, minVersion.Build, minVersion.Revision);
        return hr;
    }

    if (DecideRedirection())
    {
        return 1;
    }

    // Connect the Activated event, to allow for this instance of the app
    // getting reactivated as a result of multi-instance redirection.
    AppInstance thisInstance = AppInstance::GetCurrent();
    auto activationToken = thisInstance.Activated(
        auto_revoke, [&thisInstance](
            const auto& sender, const AppActivationArguments& args)
        { OnActivated(sender, args); }
    );

    // Carry on with regular Windows initialization.
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_CLASSNAME, szWindowClass, MAX_LOADSTRING);
    RegisterWindowClass(hInstance);
    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    MddBootstrapShutdown();
    return (int)msg.wParam;
}

void OnActivated(const IInspectable&, const AppActivationArguments& args)
{
    int const arraysize = 4096;
    WCHAR szTmp[arraysize];
    size_t cbTmp = arraysize * sizeof(WCHAR);
    StringCbPrintf(szTmp, cbTmp, L"OnActivated (%d)", activationCount++);

    ExtendedActivationKind kind = args.Kind();
    if (kind == ExtendedActivationKind::Launch)
    {
        ReportLaunchArgs(szTmp, args);
    }
    else if (kind == ExtendedActivationKind::File)
    {
        ReportFileArgs(szTmp, args);
    }
}

Lógica de redireccionamiento basada en el tipo de activación

En este ejemplo, la aplicación registra un controlador para el evento Activado y también comprueba que los argumentos del evento de activación decidan si deben redirigir la activación a otra instancia.

Para la mayoría de los tipos de activaciones, la aplicación continúa con su proceso de inicialización normal. Sin embargo, si la activación se debe a que se abre un tipo de archivo asociado y si otra instancia de esta aplicación ya tiene el archivo abierto, la instancia actual redirigirá la activación a la instancia existente y se cerrará.

Esta aplicación usa el registro de claves para determinar qué archivos están abiertos en qué instancias. Cuando una instancia abre un archivo, registra una clave que incluye ese nombre de archivo. A continuación, otras instancias pueden examinar las claves registradas, buscar nombres de archivo concretos y registrarse como instancia de ese archivo si ya no tiene ninguna otra instancia.

Tenga en cuenta que, aunque el propio registro de claves forma parte de la API del ciclo de vida de la aplicación en los SDK de aplicaciones para Windows, el contenido de la clave solo se especifica dentro de la propia aplicación. Una aplicación no necesita registrar un nombre de archivo ni ningún otro dato significativo. Sin embargo, esta aplicación ha decidido realizar un seguimiento de los archivos abiertos a través de claves en función de sus necesidades específicas y de los flujos de trabajo admitidos.

bool DecideRedirection()
{
    // Get the current executable filesystem path, so we can
    // use it later in registering for activation kinds.
    GetModuleFileName(NULL, szExePath, MAX_PATH);
    wcscpy_s(szExePathAndIconIndex, szExePath);
    wcscat_s(szExePathAndIconIndex, L",1");

    // Find out what kind of activation this is.
    AppActivationArguments args = AppInstance::GetCurrent().GetActivatedEventArgs();
    ExtendedActivationKind kind = args.Kind();
    if (kind == ExtendedActivationKind::Launch)
    {
        ReportLaunchArgs(L"WinMain", args);
    }
    else if (kind == ExtendedActivationKind::File)
    {
        ReportFileArgs(L"WinMain", args);

        try
        {
            // This is a file activation: here we'll get the file information,
            // and register the file name as our instance key.
            IFileActivatedEventArgs fileArgs = args.Data().as<IFileActivatedEventArgs>();
            if (fileArgs != NULL)
            {
                IStorageItem file = fileArgs.Files().GetAt(0);
                AppInstance keyInstance = AppInstance::FindOrRegisterForKey(file.Name());
                OutputFormattedMessage(
                    L"Registered key = %ls", keyInstance.Key().c_str());

                // If we successfully registered the file name, we must be the
                // only instance running that was activated for this file.
                if (keyInstance.IsCurrent())
                {
                    // Report successful file name key registration.
                    OutputFormattedMessage(
                        L"IsCurrent=true; registered this instance for %ls",
                        file.Name().c_str());
                }
                else
                {
                    keyInstance.RedirectActivationToAsync(args).get();
                    return true;
                }
            }
        }
        catch (...)
        {
            OutputErrorString(L"Error getting instance information");
        }
    }
    return false;
}

Redireccionamiento arbitrario

Este ejemplo amplía el anterior añadiendo reglas de redireccionamiento más sofisticadas. La aplicación sigue realizando la comprobación de archivos abiertos del ejemplo anterior. Sin embargo, mientras que en el ejemplo anterior siempre se creaba una nueva instancia si no se redirigía en función de la comprobación de archivos abiertos, en este ejemplo se añade el concepto de instancia "reutilizable". Si se encuentra una instancia reutilizable, la instancia actual redirige a la instancia reutilizable y sale. De lo contrario, se registra como reutilizable y continúa con su inicialización normal.

De nuevo, tenga en cuenta que el concepto de una instancia "reutilizable" no existe en la API del ciclo de vida de la aplicación; se crea y se usa solo dentro de la propia aplicación.

int APIENTRY wWinMain(
    _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    // Initialize COM.
    winrt::init_apartment();

    AppActivationArguments activationArgs =
        AppInstance::GetCurrent().GetActivatedEventArgs();

    // Check for any specific activation kind we care about.
    ExtendedActivationKind kind = activationArgs.Kind;
    if (kind == ExtendedActivationKind::File)
    {
        // etc... as in previous scenario.
    }
    else
    {
        // For other activation kinds, we'll trawl all instances to see if
        // any are suitable for redirecting this request. First, get a list
        // of all running instances of this app.
        auto instances = AppInstance::GetInstances();

        // In the simple case, we'll redirect to any other instance.
        AppInstance instance = instances.GetAt(0);

        // If the app re-registers re-usable instances, we can filter for these instead.
        // In this example, the app uses the string "REUSABLE" to indicate to itself
        // that it can redirect to a particular instance.
        bool isFound = false;
        for (AppInstance instance : instances)
        {
            if (instance.Key == L"REUSABLE")
            {
                isFound = true;
                instance.RedirectActivationToAsync(activationArgs).get();
                break;
            }
        }
        if (!isFound)
        {
            // We'll register this as a reusable instance, and then
            // go ahead and do normal initialization.
            winrt::hstring szKey = L"REUSABLE";
            AppInstance::FindOrRegisterForKey(szKey);
            RegisterClassAndStartMessagePump(hInstance, nCmdShow);
        }
    }
    return 1;
}

Orquestación de redireccionamiento

En este ejemplo se vuelve a agregar un comportamiento de redireccionamiento más sofisticado. Aquí, una instancia de aplicación puede registrarse como la instancia que controla todas las activaciones de un tipo específico. Cuando una instancia de una aplicación recibe una activación de Protocol, primero comprueba si hay una instancia que ya se ha registrado para controlar activaciones de Protocol. Si encuentra una, redirige la activación a esa instancia. Si no es así, la instancia actual se registra para activaciones de Protocol y, a continuación, aplica lógica adicional (no se muestra) que puede redirigir la activación por algún otro motivo.

void OnActivated(const IInspectable&, const AppActivationArguments& args)
{
    const ExtendedActivationKind kind = args.Kind;

    // For example, we might want to redirect protocol activations.
    if (kind == ExtendedActivationKind::Protocol)
    {
        auto protocolArgs = args.Data().as<ProtocolActivatedEventArgs>();
        Uri uri = protocolArgs.Uri();

        // We'll try to find the instance that handles protocol activations.
        // If there isn't one, then this instance will take over that duty.
        auto instance = AppInstance::FindOrRegisterForKey(uri.AbsoluteUri());
        if (!instance.IsCurrent)
        {
            instance.RedirectActivationToAsync(args).get();
        }
        else
        {
            DoSomethingWithProtocolArgs(uri);
        }
    }
    else
    {
        // In this example, this instance of the app handles all other
        // activation kinds.
        DoSomethingWithNewActivationArgs(args);
    }
}

A diferencia de la versión de UWP de RedirectActivationTo, la implementación del SDK de aplicaciones para Windows de RedirectActivationToAsync requiere transferir explícitamente argumentos de evento al redirigir las activaciones. Esto es necesario porque, mientras que UWP controla estrechamente las activaciones y puede garantizar que los argumentos de activación correctos se transfieren a las instancias correctas, la versión del SDK de aplicaciones para Windows admite muchas plataformas y no puede basarse en características específicas de UWP. Una ventaja de este modelo es que las aplicaciones que usan el SDK de aplicaciones para Windows pueden modificar o reemplazar los argumentos que se transferirán a la instancia de destino.

Redireccionamiento sin bloqueo

La mayoría de las aplicaciones querrán redirigir lo antes posible, antes de realizar un trabajo de inicialización innecesario. Para algunos tipos de aplicación, la lógica de inicialización se ejecuta en un subproceso STA, que no debe bloquearse. El método AppInstance.RedirectActivationToAsync es asincrónico y la aplicación que realiza la llamada debe esperar a que se complete el método; de lo contrario, se producirá un error en el redireccionamiento. Sin embargo, la espera en una llamada asincrónica bloqueará el STA. En estas situaciones, llame a RedirectActivationToAsync en otro subproceso y establezca un evento cuando se complete la llamada. A continuación, espere en ese evento mediante API sin bloqueo, como CoWaitForMultipleObjects. Este es un ejemplo de C# para una aplicación WPF.

private static bool DecideRedirection()
{
    bool isRedirect = false;

    // Find out what kind of activation this is.
    AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
    ExtendedActivationKind kind = args.Kind;
    if (kind == ExtendedActivationKind.File)
    {
        try
        {
            // This is a file activation: here we'll get the file information,
            // and register the file name as our instance key.
            if (args.Data is IFileActivatedEventArgs fileArgs)
            {
                IStorageItem file = fileArgs.Files[0];
                AppInstance keyInstance = AppInstance.FindOrRegisterForKey(file.Name);

                // If we successfully registered the file name, we must be the
                // only instance running that was activated for this file.
                if (keyInstance.IsCurrent)
                {
                    // Hook up the Activated event, to allow for this instance of the app
                    // getting reactivated as a result of multi-instance redirection.
                    keyInstance.Activated += OnActivated;
                }
                else
                {
                    isRedirect = true;

                    // Ensure we don't block the STA, by doing the redirect operation
                    // in another thread, and using an event to signal when it has completed.
                    redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null);
                    if (redirectEventHandle != IntPtr.Zero)
                    {
                        Task.Run(() =>
                        {
                            keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
                            SetEvent(redirectEventHandle);
                        });
                        uint CWMO_DEFAULT = 0;
                        uint INFINITE = 0xFFFFFFFF;
                        _ = CoWaitForMultipleObjects(
                            CWMO_DEFAULT, INFINITE, 1, 
                            new IntPtr[] { redirectEventHandle }, out uint handleIndex);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error getting instance information: {ex.Message}");
        }
    }

    return isRedirect;
}

Anulación del registro de redireccionamiento

Las aplicaciones que han registrado una clave pueden anular el registro de esa clave en cualquier momento. En este ejemplo se supone que la instancia actual había registrado previamente una clave que indica que tenía un archivo específico abierto, lo que significa que los intentos posteriores de abrir ese archivo se redirigirían a ella. Cuando se cierra ese archivo, se debe eliminar la clave que contiene el nombre de archivo.

void CALLBACK OnFileClosed(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    AppInstance::GetCurrent().UnregisterKey();
}

Advertencia

Aunque el registro de las claves se anula automáticamente cuando finaliza su proceso, se pueden dar condiciones de carrera cuando otra instancia puede haber iniciado un redireccionamiento a la instancia terminada antes de que se anulara el registro de la instancia terminada. Para mitigar esta posibilidad, una aplicación puede usar UnregisterKey para anular manualmente el registro de su clave antes de finalizar, lo que da a la aplicación la oportunidad de redirigir las activaciones a otra aplicación que no está en proceso de salida.

Información de instancia

La clase Microsoft.Windows.AppLifeycle.AppInstance representa una única instancia de una aplicación. En la versión preliminar actual, AppInstance solo incluye los métodos y propiedades necesarios para admitir el redireccionamiento de activación. En versiones posteriores, AppInstance se ampliará para incluir otros métodos y propiedades relevantes para una instancia de aplicación.

void DumpExistingInstances()
{
    for (AppInstance const& instance : AppInstance::GetInstances())
    {
        std::wostringstream sStream;
        sStream << L"Instance: ProcessId = " << instance.ProcessId
            << L", Key = " << instance.Key().c_str() << std::endl;
        ::OutputDebugString(sStream.str().c_str());
    }
}