Creazione di istanze dell'app con l'API del ciclo di vita dell'app

Il modello di istanze di un'app determina se più istanze del processo dell'app possono essere eseguite contemporaneamente.

Prerequisiti

Per usare l'API del ciclo di vita dell'app nella SDK per app di Windows:

  1. Scaricare e installare la versione più recente del SDK per app di Windows. Per altre informazioni, vedere Installare gli strumenti per il SDK per app di Windows.
  2. Seguire le istruzioni per creare il primo progetto WinUI 3 o per usare il SDK per app di Windows in un progetto esistente.

App a istanza singola

Nota

Per un esempio di come implementare una singola istanza in un'app WinUI 3 con C#, vedere Creazione dell'app a istanza singola nel blog per sviluppatori windows.

Le app vengono a istanza singola se è possibile eseguire un solo processo principale alla volta. Il tentativo di avviare una seconda istanza di un'app a istanza singola comporta in genere l'attivazione della finestra principale della prima istanza. Si noti che questo vale solo per il processo principale. Le app a istanza singola possono creare più processi in background e comunque essere considerate singole istanze.

Le app UWP sono a istanza singola per impostazione predefinita. ma avere la possibilità di diventare multiistanza decidendo in fase di avvio se creare un'istanza aggiuntiva o attivare un'istanza esistente.

L'app Windows Mail è un buon esempio di una singola app a istanza. Quando si avvia Mail per la prima volta, verrà creata una nuova finestra. Se si tenta di avviare nuovamente Mail, verrà attivata la finestra Posta esistente.

App a istanze multipla

Le app vengono eseguite più istanze se il processo principale può essere eseguito più volte contemporaneamente. Il tentativo di avviare una seconda istanza di un'app a istanze multipla crea un nuovo processo e una finestra principale.

Tradizionalmente, le app decompresse sono multiistanza per impostazione predefinita, ma possono implementare la creazione di istanze singole quando necessariamente. In genere questa operazione viene eseguita usando un singolo mutex denominato per indicare se un'app è già in esecuzione.

Blocco note è un buon esempio di un'app a istanze multipla. Ogni volta che si tenta di avviare Blocco note, verrà creata una nuova istanza di Blocco note indipendentemente dal numero di istanze già in esecuzione.

Differenze tra il SDK per app di Windows instancing e la creazione di istanze della piattaforma UWP

Il comportamento di creazione di istanze nella SDK per app di Windows si basa sul modello, la classe UWP, ma con alcune differenze principali:

Classe AppInstance

Elenco di istanze

  • UWP: GetInstances restituisce solo le istanze registrate esplicitamente dall'app per il potenziale reindirizzamento.
  • SDK per app di Windows: GetInstances restituisce tutte le istanze in esecuzione dell'app che usano l'API AppInstance, indipendentemente dal fatto che abbiano registrato o meno una chiave. Può includere l'istanza corrente. Se si desidera che l'istanza corrente venga inclusa nell'elenco, chiamare AppInstance.GetCurrent. Gli elenchi separati vengono mantenuti per versioni diverse della stessa app, nonché per le istanze di app avviate da utenti diversi.

Registrazione delle chiavi

Ogni istanza di un'app a istanze multipla può registrare una chiave arbitraria tramite il FindOrRegisterForKey metodo . Le chiavi non hanno alcun significato intrinseco; le app possono usare chiavi in qualsiasi forma o modo desiderato.

Un'istanza di un'app può impostare la chiave in qualsiasi momento, ma per ogni istanza è consentita una sola chiave; l'impostazione di un nuovo valore sovrascrive il valore precedente.

Un'istanza di un'app non può impostare la chiave sullo stesso valore già registrato da un'altra istanza. Se si tenta di registrare una chiave esistente, FindOrRegisterForKey verrà restituita l'istanza dell'app che ha già registrato tale chiave.

  • UWP: un'istanza deve registrare una chiave per essere inclusa nell'elenco restituito da GetInstances.
  • SDK per app di Windows: la registrazione di una chiave viene disaccoppiata dall'elenco di istanze. Un'istanza di non deve registrare una chiave per essere inclusa nell'elenco.

Annullamento della registrazione delle chiavi

Un'istanza di un'app può annullare la registrazione della chiave.

  • UWP: quando un'istanza annulla la registrazione della chiave, non è più disponibile per il reindirizzamento dell'attivazione e non è inclusa nell'elenco delle istanze restituite da GetInstances.
  • SDK per app di Windows: un'istanza che ha annullata la registrazione della chiave è ancora disponibile per il reindirizzamento dell'attivazione ed è ancora inclusa nell'elenco delle istanze restituite da GetInstances.

Destinazioni di reindirizzamento dell'istanza

Più istanze di un'app possono attivarsi tra loro, un processo denominato "reindirizzamento attivazione". Ad esempio, un'app potrebbe implementare una singola istanza inizializzandosi solo se non vengono trovate altre istanze dell'app all'avvio e reindirizzare e uscire se esiste un'altra istanza. Le app a istanze multipla possono reindirizzare le attivazioni quando appropriato in base alla logica di business dell'app. Quando un'attivazione viene reindirizzata a un'altra istanza, usa il callback dell'istanza Activated , lo stesso callback usato in tutti gli altri scenari di attivazione.

  • UWP: solo le istanze che hanno registrato una chiave possono essere una destinazione per il reindirizzamento.
  • SDK per app di Windows: qualsiasi istanza può essere una destinazione di reindirizzamento, indipendentemente dal fatto che abbia o meno una chiave registrata.

Comportamento post-reindirizzamento

  • UWP: il reindirizzamento è un'operazione terminale; l'app viene terminata dopo il reindirizzamento dell'attivazione, anche se il reindirizzamento non è riuscito.

  • SDK per app di Windows: nel SDK per app di Windows il reindirizzamento non è un'operazione terminale. Questo in parte riflette i potenziali problemi nella terminazione arbitraria di un'app Win32 che potrebbe aver già allocato memoria, ma consente anche il supporto di scenari di reindirizzamento più sofisticati. Si consideri un'app a istanze multipla in cui un'istanza riceve una richiesta di attivazione durante l'esecuzione di una grande quantità di lavoro a elevato utilizzo della CPU. Tale app può reindirizzare la richiesta di attivazione a un'altra istanza e continuare l'elaborazione. Questo scenario non sarebbe possibile se l'app è stata terminata dopo il reindirizzamento.

Una richiesta di attivazione può essere reindirizzata più volte. L'istanza A potrebbe reindirizzare all'istanza B, che a sua volta potrebbe reindirizzare all'istanza C. SDK per app di Windows app che sfruttano questa funzionalità deve proteggersi dal reindirizzamento circolare. Se C reindirizza a A nell'esempio precedente, esiste un potenziale ciclo di attivazione infinito. Spetta all'app determinare come gestire il reindirizzamento circolare a seconda dei flussi di lavoro supportati dall'app.

Eventi di attivazione

Per gestire la riattivazione, l'app può registrarsi per un evento Activated.

Esempi

Gestione delle attivazioni

Questo esempio illustra come un'app viene registrata per e gestisce un Activated evento. Quando riceve un Activated evento, questa app usa gli argomenti dell'evento per determinare quale tipo di azione ha causato l'attivazione e risponde in modo appropriato.

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

Logica di reindirizzamento basata sul tipo di attivazione

In questo esempio, l'app registra un gestore per l'evento Activated e controlla anche la presenza degli argomenti dell'evento di attivazione per decidere se reindirizzare l'attivazione a un'altra istanza.

Per la maggior parte dei tipi di attivazioni, l'app continua con il processo di inizializzazione regolare. Tuttavia, se l'attivazione è stata causata dall'apertura di un tipo di file associato e se un'altra istanza di questa app ha già aperto il file, l'istanza corrente reindirizzerà l'attivazione all'istanza esistente e si chiuderà.

Questa app usa la registrazione della chiave per determinare quali file sono aperti in quali istanze. Quando un'istanza apre un file, registra una chiave che include tale nome file. Altre istanze possono quindi esaminare le chiavi registrate e cercare nomi di file specifici e registrarsi come istanza del file se non è già presente un'altra istanza.

Si noti che, anche se la registrazione della chiave stessa fa parte dell'API del ciclo di vita dell'app nel SDK per app di Windows, il contenuto della chiave viene specificato solo all'interno dell'app stessa. Un'app non deve registrare un nome di file o altri dati significativi. Questa app, tuttavia, ha deciso di tenere traccia dei file aperti tramite chiavi in base alle esigenze specifiche e ai flussi di lavoro supportati.

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

Reindirizzamento arbitrario

Questo esempio si espande nell'esempio precedente aggiungendo regole di reindirizzamento più sofisticate. L'app esegue comunque il controllo del file aperto dell'esempio precedente. Tuttavia, quando l'esempio precedente crea sempre una nuova istanza se non viene reindirizzato in base al controllo del file aperto, in questo esempio viene aggiunto il concetto di istanza "riutilizzabile". Se viene trovata un'istanza riutilizzabile, l'istanza corrente reindirizza all'istanza riutilizzabile ed esce. In caso contrario, si registra come riutilizzabile e continua con la normale inizializzazione.

Anche in questo caso, si noti che il concetto di istanza "riutilizzabile" non esiste nell'API del ciclo di vita dell'app; viene creato e usato solo all'interno dell'app stessa.

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

Orchestrazione del reindirizzamento

Questo esempio aggiunge di nuovo un comportamento di reindirizzamento più sofisticato. In questo caso, un'istanza dell'app può registrarsi come istanza che gestisce tutte le attivazioni di un tipo specifico. Quando un'istanza di un'app riceve un'attivazione Protocol , verifica innanzitutto la presenza di un'istanza già registrata per gestire Protocol le attivazioni. Se ne trova uno, reindirizza l'attivazione a tale istanza. In caso contrario, l'istanza corrente si registra per Protocol le attivazioni e quindi applica logica aggiuntiva (non visualizzata) che potrebbe reindirizzare l'attivazione per altri motivi.

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 differenza della versione UWP di RedirectActivationTo, l'implementazione di SDK per app di Windows di RedirectActivationToAsync richiede il passaggio esplicito di argomenti evento durante il reindirizzamento delle attivazioni. Ciò è necessario perché la piattaforma UWP controlla rigorosamente le attivazioni e può garantire che gli argomenti di attivazione corretti vengano passati alle istanze corrette, la versione del SDK per app di Windows supporta molte piattaforme e non può basarsi su funzionalità specifiche della piattaforma UWP. Uno dei vantaggi di questo modello è che le app che usano il SDK per app di Windows hanno la possibilità di modificare o sostituire gli argomenti che verranno passati all'istanza di destinazione.

Reindirizzamento senza blocco

La maggior parte delle app vuole reindirizzare il prima possibile, prima di eseguire operazioni di inizializzazione non necessarie. Per alcuni tipi di app, la logica di inizializzazione viene eseguita in un thread STA, che non deve essere bloccata. Il metodo AppInstance.RedirectActivationToAsync è asincrono e l'app chiamante deve attendere il completamento del metodo. In caso contrario, il reindirizzamento avrà esito negativo. Tuttavia, l'attesa su una chiamata asincrona bloccherà l'sta. In queste situazioni, chiamare RedirectActivationToAsync in un altro thread e impostare un evento al termine della chiamata. Attendere quindi l'evento usando API non bloccanti, ad esempio CoWaitForMultipleObjects. Ecco un esempio C# per un'app 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;
}

Annullare la registrazione per il reindirizzamento

Le app che hanno registrato una chiave possono annullare la registrazione della chiave in qualsiasi momento. In questo esempio si presuppone che l'istanza corrente avesse registrato in precedenza una chiave che indica che aveva un file specifico aperto, ovvero i successivi tentativi di aprire tale file verrebbero reindirizzati. Quando il file viene chiuso, la chiave che contiene il nome file deve essere eliminata.

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

Avviso

Anche se le chiavi vengono annullate automaticamente quando termina il processo, è possibile che le race condition siano possibili in cui un'altra istanza può aver avviato un reindirizzamento all'istanza terminata prima che l'istanza terminata non sia stata registrata. Per attenuare questa possibilità, un'app può usare UnregisterKey per annullare manualmente la registrazione della chiave prima che venga terminata, dando all'app la possibilità di reindirizzare le attivazioni a un'altra app che non è in fase di uscita.

Informazioni sull'istanza

La classe Microsoft.Windows.AppLifeycle.AppInstance rappresenta una singola istanza di un'app. Nell'anteprima corrente sono AppInstance inclusi solo i metodi e le proprietà necessari per supportare il reindirizzamento dell'attivazione. Nelle versioni successive si AppInstance espanderà per includere altri metodi e proprietà rilevanti per un'istanza dell'app.

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