Instanciação de aplicativo com a API de ciclo de vida do aplicativo

O modelo de instância de um aplicativo determina se diversas instâncias do processo do aplicativo podem ser executadas ao mesmo tempo. A API do ciclo de vida do aplicativo no SDK do Aplicativo Windows fornece uma maneira de controlar quantas instâncias do seu aplicativo podem ser executadas ao mesmo tempo e redirecionar ativações para outras instâncias quando necessário.

Este artigo descreve como usar a API do ciclo de vida do aplicativo para controlar a instanciação do aplicativo em seus aplicativos WinUI.

Pré-requisitos

Para usar a API do ciclo de vida do aplicativo em aplicativos WinUI 3:

Aplicativos de instância única

Os aplicativos são de instância única se puder haver somente um processo principal em execução por vez. Normalmente, a tentativa de iniciar uma segunda instância de um aplicativo de instância única resulta na ativação da janela principal da primeira instância. Isso só se aplica ao processo principal. Os aplicativos de instância única podem criar vários processos no segundo plano e ainda ser considerados instâncias únicas.

Os aplicativos WinUI são de instância única por padrão, mas têm a capacidade de se tornar de várias instâncias, decidindo no momento da inicialização se deseja criar uma instância adicional ou ativar uma instância existente.

O aplicativo Microsoft Photos é um bom exemplo de um aplicativo WinUI de instância única. Ao iniciar o Fotos pela primeira vez, uma nova janela será criada. Se você tentar iniciar o Fotos novamente, a janela existente será ativada.

Para obter um exemplo de como implementar a instanciação única em um aplicativo WinUI 3 com C#, consulte Criar um aplicativo WinUI de instância única.

Aplicativos multi-instâncias

Os aplicativos são multi-instâncias se o processo principal puder ser executado várias vezes simultaneamente. A tentativa de iniciar uma segunda instância de um aplicativo com multi-instâncias cria um novo processo e uma nova janela principal.

Tradicionalmente, os aplicativos descompactados são multi-instâncias por padrão, mas podem implementar a instância única quando necessário. Normalmente, isso é feito com um único mutex nomeado para indicar se um aplicativo já está em execução.

O Bloco de notas é um bom exemplo de um aplicativo multi-instâncias. Cada vez que você tentar iniciar o Bloco de notas, uma nova instância do Bloco de notas será criada, independentemente de quantas instâncias já estejam em execução.

Diferença entre a instanciação do SDK do Aplicativo Windows e a instanciação UWP

O comportamento de instanciação no SDK de Aplicativo Windows é baseado no modelo, classe da UWP, mas com diferenças importantes:

Classe AppInstance

Lista de instâncias

  • UWP: GetInstances retorna somente as instâncias que o aplicativo registrou explicitamente para possível redirecionamento.
  • SDK de Aplicativos do Windows: GetInstances retorna todas as instâncias em execução do aplicativo que estão usando a API AppInstance, independentemente de terem ou não registrado uma chave. Isso pode também incluir a instância atual. Se quiser que a instância atual seja incluída na lista, chame AppInstance.GetCurrent. Há listas separadas para diferentes versões do mesmo aplicativo, bem como instâncias de aplicativos iniciados por usuários diferentes.

Registro de chaves

Cada instância de um aplicativo com várias instâncias pode registrar uma chave arbitrária por meio do método FindOrRegisterForKey. As chaves não têm significado inerente; os aplicativos podem usar chaves como desejarem.

Uma instância de um aplicativo pode definir sua chave a qualquer momento, mas somente uma chave é permitida para cada instância; a definição de um novo valor substitui o valor anterior.

A instância de um aplicativo não pode definir sua chave com o mesmo valor que outra instância já registrou. A tentativa de registrar uma chave existente fará com que FindOrRegisterForKey retorne a instância do aplicativo que já registrou essa chave.

  • UWP: uma instância deve registrar uma chave para ser incluída na lista retornada de GetInstances.
  • SDK do Aplicativo do Windows: o registro de uma chave é desacoplado da lista de instâncias. Uma instância não precisa registrar uma chave para entrar na lista.

Cancelamento do registro de chaves

A instância de um aplicativo pode cancelar o registro de sua chave.

  • UWP: quando uma instância cancela o registro da sua chave, ela não fica mais disponível para redirecionamento de ativação e não é incluída na lista de instâncias retornadas de GetInstances.
  • SDK do Aplicativo do Windows: uma instância que cancelou o registro de sua chave ainda está disponível para redirecionamento de ativação e ainda está incluída na lista de instâncias retornadas de GetInstances.

Destinos para redirecionamento de instância

Várias instâncias de um aplicativo podem ativar umas às outras. Esse processo é chamado "redirecionamento de ativação". Por exemplo, um aplicativo só pode implementar uma única instância inicializando a si mesmo se nenhuma outra instância do aplicativo for encontrada na inicialização e, em vez disso, redirecionar e sair se outra instância existir. Os aplicativos multi-instâncias podem redirecionar ativações quando apropriado de acordo com a lógica de negócios desse aplicativo. Quando uma ativação é redirecionada para outra instância, ela usa o retorno de chamada dessa instância Activated, o mesmo retorno de chamada usado em todos os outros cenários de ativação.

  • UWP: somente instâncias que registraram uma chave podem ser destino de redirecionamento.
  • SDK do Aplicativo do Windows: qualquer instância pode ser um destino de redirecionamento, tenha ou não uma chave registrada.

Comportamento de pós-redirecionamento

  • UWP: o redirecionamento é uma operação de terminal; o aplicativo é encerrado após o redirecionamento da ativação, mesmo que o redirecionamento tenha falhado.

  • SDK do Aplicativo do Windows: no SDK do Aplicativo do Windows, o redirecionamento não é uma operação de terminal. Em parte, isso reflete os problemas potenciais em encerrar arbitrariamente um aplicativo Win32 que pode já ter alocado memória, mas também permite o suporte a cenários de redirecionamento mais sofisticados. Considere um aplicativo multi-instâncias em que uma instância recebe uma solicitação de ativação enquanto executa uma grande quantidade de trabalho intensivo de CPU. Esse aplicativo pode redirecionar a solicitação de ativação a outra instância e continuar seu processamento. Esse cenário não seria possível se o aplicativo fosse encerrado depois do redirecionamento.

Uma solicitação de ativação pode ser redirecionada diversas vezes. A instância A poderia redirecionar para a instância B, que por sua vez poderia redirecionar para a instância C. Os aplicativos do SDK do Aplicativo Windows que aproveitam essa funcionalidade devem se proteger contra o redirecionamento circular; se C redirecionar para A no exemplo acima, haverá um loop de ativação infinito potencial. O aplicativo determina como lidar com o redirecionamento circular, dependendo do que faz sentido para os fluxos de trabalho suportados pelo aplicativo.

Eventos de ativação

Para lidar com a reativação, o aplicativo pode se registrar em um evento Ativado.

Exemplos

Como lidar com ativações

Este exemplo demonstra como um aplicativo registra e manipula um evento Activated. Quando recebe um evento Activated, este aplicativo usa os argumentos de evento para determinar que tipo de ação causou a ativação e responde adequadamente.

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 redirecionamento com base no tipo de ativação

Neste exemplo, o aplicativo registra um manipulador para o evento Activated e também verifica os args de eventos de ativação para decidir se quer redirecionar a ativação para outra instância.

Para a maioria dos tipos de ativações, o aplicativo continua com o processo de inicialização normal. No entanto, se a ativação foi causada por um tipo de arquivo associado ser aberto, e se outra instância deste aplicativo já tiver o arquivo aberto, a instância atual redirecionará a ativação à instância existente e sair.

Este aplicativo utiliza o registro de chave para determinar quais arquivos estão abertos em quais instâncias. Quando abre um arquivo, a instância registra uma chave que inclui esse nome de arquivo. Outras instâncias podem examinar as chaves registradas e buscar nomes de arquivos específicos e registrar-se como instância desse arquivo, se nenhuma outra instância já tiver.

Observe que, embora o próprio registro de chave faça parte da API do ciclo de vida do aplicativo no SDK do Aplicativo Windows, o conteúdo da chave é especificado somente no aplicativo em si. Um aplicativo não precisa registrar um nome de arquivo ou outros dados significativos. Porém, este aplicativo decidiu rastrear arquivos abertos por meio de chaves com base nas suas necessidades específicas e fluxos de trabalho compatíveis.

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;
}

Redirecionamento arbitrário

Este exemplo expande o exemplo anterior ao adicionar regras de redirecionamento mais sofisticadas. O aplicativo ainda executa a verificação do arquivo aberto do exemplo anterior. Porém, enquanto o exemplo anterior sempre criaria uma nova instância se não redirecionasse com base na verificação de arquivo aberto, este exemplo adiciona o conceito de uma instância "reutilizável". Se for encontrada uma instância reutilizável, a instância atual redirecionará para a instância reutilizável e será encerrada. Caso contrário, ela se registra como reutilizável e continua com sua inicialização normal.

Novamente, observe que o conceito de uma instância "reutilizável" não existe na API do ciclo de vida do aplicativo; ele é criado e usado somente dentro do aplicativo em si.

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;
}

Orquestração de redirecionamento

Novamente, este exemplo adiciona um comportamento de redirecionamento mais sofisticado. Aqui, a instância de aplicativo pode se registrar como a instância que manipula todas as ativações de um tipo específico. Quando a instância de um aplicativo recebe uma ativação Protocol, ela verifica primeiro se há uma instância que já se registrou para tratar de ativações Protocol. Caso encontre, ela redirecionará a ativação a essa instância. Caso contrário, a instância atual se registra para ativações Protocol e, depois, aplica lógica adicional (não mostrada) que pode redirecionar a ativação por outro 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);
    }
}

Ao contrário da versão UWP do RedirectActivationTo, a implementação do SDK do Aplicativo Windows de RedirectActivationToAsync exige passar explicitamente argumentos de evento ao redirecionar ativações. Isso é necessário porque, enquanto a UWP controla rigorosamente as ativações e pode garantir que os argumentos de ativação corretos sejam passados às instâncias corretas, a versão do SDK do Aplicativo Windows dá suporte a muitas plataformas e não pode depender de recursos específicos da UWP. Uma vantagem desse modelo é que os aplicativos que usam o SDK do Aplicativo Windows têm a chance de modificar ou substituir os argumentos que serão passados à instância de destino.

Redirecionamento sem bloqueio

A maioria dos aplicativos quer redirecionar o mais rápido possível, antes de fazer o trabalho de inicialização desnecessário. Para certos tipos de aplicativo, a lógica de inicialização é executada em um thread STA, que não deve ser bloqueado. O método AppInstance.RedirectActivationToAsync é assíncrono e o aplicativo de chamada deve aguardar a conclusão do método; caso contrário, o redirecionamento falhará. Entretanto, aguardar em uma chamada assíncrona bloqueará o STA. Nesses casos, chame RedirectActivationToAsync em outro thread e defina um evento quando a chamada for concluída. Depois, aguarde esse evento usando APIs sem bloqueio, como CoWaitForMultipleObjects. Veja este exemplo de C# para um aplicativo 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;
}

Cancelar registro para redirecionamento

Os aplicativos que tiverem registrado uma chave podem cancelar o registro dessa chave a qualquer momento. Este exemplo presume que a instância atual já registrou uma chave indicando que ela tinha um arquivo específico aberto, o que significa que as tentativas subsequentes de abrir esse arquivo seriam redirecionadas a ele. Quando esse arquivo é fechado, a chave que contém o nome do arquivo deverá ser excluída.

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

Aviso

Embora as chaves sejam canceladas automaticamente quando seu processo é encerrado, as condições de corrida são possíveis quando outra instância puder ter iniciado um redirecionamento para a instância encerrada antes que a instância encerrada tivesse o registro cancelado. Para mitigar essa possibilidade, um aplicativo pode usar UnregisterKey para cancelar manualmente o registro da sua chave antes que ela seja encerrada, dando ao aplicativo a chance de redirecionar ativações a outro aplicativo que não esteja em processo de encerramento.

Informações sobre a instância

A classe Microsoft.Windows.AppLifeycle.AppInstance representa uma única instância de um aplicativo. Na visualização atual, AppInstance inclui somente os métodos e as propriedades necessárias para dar suporte ao redirecionamento de ativação. Em versões posteriores, AppInstance será expandido e incluirá outros métodos e propriedades relevantes para uma instância de aplicativo.

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());
    }
}

Criar um aplicativo WinUI de instância única

Microsoft.Windows.AppLifeycle.AppInstance