Verfügbarmachen von .NET-Aufgaben als asynchrone WinRT-Vorgänge

Im Blogbeitrag Schnelle und flüssige Apps mit Asynchronität in der Windows-Runtime haben wir die neuen async- und await-Schlüsselwörter in C# und Visual Basic behandelt und erörtert, wie diese für asynchrone WinRT-Vorgänge (Windows-Runtime) verwendet werden können.

Mit Unterstützung durch die .NET-Basisklassenbibliotheken (Base Class Libraries, BCL), können Sie diese Schlüsselwörter auch für das Entwickeln asynchroner Vorgänge verwenden, die anschließend über WinRT für andere Komponenten verfügbar gemacht werden, die in anderen Sprachen erstellt wurden. In diesem Beitrag wird dies näher erläutert. (Allgemeine Informationen zum Implementieren von WinRT-Komponenten mit C# oder Visual Basic finden Sie unter Erstellen von Komponenten für Windows-Runtime in C# und Visual Basic.)

Beschäftigen wir und zunächst mit der Form von asynchronen APIs in WinRT.

Asynchrone WinRT-Schnittstellen

WinRT verfügt über verschiedene Schnittstellen in Bezug auf asynchrone Vorgänge. Die erste Schnittstelle ist IAsyncInfo, die von jedem gültigen asynchronen WinRT-Vorgang implementiert wird. Sie stellt die allgemeinen Funktionen für asynchrone Vorgänge bereit, einschließlich des aktuellen Status eines Vorgangs, den Fehler eines Vorgangs (sofern dieser fehlgeschlagen ist), einer ID für den Vorgang sowie dessen Möglichkeit, einen Abbruch des Vorgangs anzufordern:

 public interface IAsyncInfo
{
    AsyncStatus Status { get; }
    Exception ErrorCode { get; }
    uint Id { get; }

    void Cancel();
    void Close();
}

Neben der Implementierung von IAsyncInfo implementiert jeder gültige asynchronen WinRT-Vorgang eine von vier zusätzlichen Schnittstellen: IAsyncAction, IAsyncActionWithProgress<TProgress> , IAsyncOperation<TResult> oder IAsyncOperationWithProgress<TResult,TProgress> . Diese zusätzlichen Schnittstellen erlauben dem Consumer, einen Callback festzulegen, der beim Abschluss des asynchronen Vorgangs aufgerufen wird. Außerdem kann der Consumer optional ein Ergebnis und/oder Fortschrittsberichte erhalten:

 // No results, no progress
public interface IAsyncAction : IAsyncInfo
{
    AsyncActionCompletedHandler Completed { get; set; }
    void GetResults();
}

// No results, with progress
public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo
{
    AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; }
    AsyncActionProgressHandler<TProgress> Progress { get; set; }
    void GetResults();
}

// With results, no progress
public interface IAsyncOperation<TResult> : IAsyncInfo
{
    AsyncOperationCompletedHandler<TResult> Completed { get; set; }
    TResult GetResults();
}

// With results, with progress
public interface IAsyncOperationWithProgress<TResult,TProgress> : IAsyncInfo
{
    AsyncOperationWithProgressCompletedHandler<TResult,TProgress> Completed { get; set; }
    AsyncOperationProgressHandler<TResult,TProgress> Progress { get; set; }
    TResult GetResults();
}

(IAsyncAction stellt auch dann eine GetResults-Methode bereit, wenn keine Ergebnisse vorhanden sind, die zurückgegeben werden können. So wird für einen fehlerhaften asynchronen Vorgang eine Methode zum Auslösen einer Ausnahme bereitgestellt, statt den Zugriff aller Consumer auf die ErrorCode-Eigenschaft von IAsyncInfo zu erzwingen. Dies ähnelt dem Vorgang, bei dem „awaiter“-Typen in C# und Visual Basic eine GetResult-Methode verfügbar machen, selbst wenn diese GetResult-Methode für die Rückgabe von void typisiert ist.)

Beim Erstellen einer WinRT-Bibliothek werden alle öffentlich verfügbar gemachten asynchronen Vorgänge in dieser Bibliothek stark typisiert, um eine dieser vier Schnittstellen zurückzugeben. Im Gegensatz hierzu folgen neue asynchrone Vorgänge, die von .NET-Bibliotheken verfügbar gemacht werden, dem aufgabenbasierten asynchronen Muster (Task-based Asynchronous Pattern, TAP) und geben Task (für Vorgänge, die kein Ergebnis zurückgeben) oder Task<TResult> (für Vorgänge, die ein Ergebnis zurückgeben) zurück.

Task und Task<TResult> implementieren diese WinRT-Schnittstellen genauso wenig, wie die Common Language Runtime (CLR) die Unterschiede implizit verdeckt (wie dies für einige Typen, z. B. den Windows.Foundation.Uri-Typ von WinRT und den System.Uri-Typ von BCL der Fall ist). Stattdessen muss explizit von einem Bereich in den anderen konvertiert werden. Im Beitrag WinRT und Await im Blickpunkt wurde gezeigt, wie die AsTask-Erweiterungsmethode in der BCL einen expliziten „umwandlungsähnlichen“ Mechanismus bereitstellt, um aus asynchronen WinRT-Schnittstellen in .NET-Aufgaben zu konvertieren. Die BCL unterstützt diesen Vorgang auch in die andere Richtung, um Methoden, um .NET-Aufgaben in asynchrone WinRT-Schnittstellen zu konvertieren.

Konvertieren mit AsAsyncAction und AsAsyncOperation

Gehen wir für diesen Blogbeitrag von einer asynchronen .NET-Methode DownloadStringAsyncInternal aus. Dieser wird eine URL zu einer Webseite übergeben, und die Methode lädt den Inhalt dieser Seite asynchron herunter und gibt ihn als Zeichenfolge zurück:

 internal static Task<string> DownloadStringAsyncInternal(string url);

Es ist gleichgültig, wie diese Methode implementiert ist. Unser Ziel ist, dies als asynchronen WinRT-Vorgang zusammenzufassen, also als eine Methode, die eine der vier zuvor genannten Schnittstellen zurückgibt. Da unsere Methode über ein Ergebnis (eine Zeichenfolge) verfügt und keine Fortschrittsberichte unterstützt, gibt unser asynchroner WinRT-Vorgang IAsyncOperation<string> zurück:

 public static IAsyncOperation<string> DownloadStringAsync(string url);

Zum Implementieren dieser Methode können wir die DownloadStringAsyncInternal-Methode aufrufen, um so das Ergebnis Task<string> zu erhalten. Anschließend müssen wir diese Aufgabe in die erforderliche IAsyncOperation<string> konvertieren. Aber wie?

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    Task<string> from = DownloadStringAsyncInternal(url);
    IAsyncOperation<string> to = ...; // TODO: how do we convert 'from'?
    return to;
}

Hierfür enthält die System.Runtime.WindowsRuntime.dll-Assembly in .NET 4.5 Erweiterungsmethoden für Task und Task<TResult> , die die erforderlichen Konvertierungen bereitstellen:

 // in System.Runtime.WindowsRuntime.dll
public static class WindowsRuntimeSystemExtensions 
{
    public static IAsyncAction AsAsyncAction(
        this Task source);
    public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
        this Task<TResult> source);
    ...
}

Diese Methoden geben eine neue IAsyncAction- oder IAsyncOperation<TResult> -Instanz zurück, die die bereitgestellten Ergebnisse Task bzw. Task<TResult> zusammenfasst (da Task<TResult> von Task abgeleitet wird, sind beide Methoden für Task<TResult> verfügbar, auch wenn es unwahrscheinlich sein dürfte, dass Sie AsAsyncAction mit einer asynchronen Methode verwenden, die Ergebnisse zurückgibt). Natürlich können Sie diese Vorgänge auch als explizite, synchrone Umwandlungen betrachten oder aus einer Entwurfsmuster-Perspektive als Adapter betrachten. Diese geben eine Instanz zurück, die die zugrunde liegende Aufgabe darstellt, jedoch die erforderliche Oberfläche für WinRT verfügbar macht. Mit solchen Erweiterungsmethoden können wir unsere DownloadStringAsync-Implementierung abschließen:

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    Task<string> from = DownloadStringAsyncInternal(url);
    IAsyncOperation<string> to = from.AsAsyncOperation();
    return to;
}

Wir können dies auch kürzer umschreiben und hervorheben, wie ähnlich dieser Vorgang einer Umwandlung ist:

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    return DownloadStringAsyncInternal(url).AsAsyncOperation();
}

DownloadStringAsyncInternal wird bereits vor dem ersten Aufruf von AsAsyncOperation aufgerufen. Dies bedeutet, dass der synchrone Aufruf von DownloadStringAsyncInternal schnell zurückgegeben werden muss, um sicherzustellen, dass die DownloadStringAsync-Wrappermethode rasch reagiert. Wenn Sie befürchten, dass der synchrone Vorgang zu lange dauern könnte oder Sie den Aufruf eines Threadpools aus anderen Gründen explizit auslagern möchten, können Sie Task.Run verwenden und die zurückgegebene Aufgabe anschließend mit AsAsyncOperation aufrufen:

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    return Task.Run(()=>DownloadStringAsyncInternal(url)).AsAsyncOperation();
}

Größere Flexibilität mit AsyncInfo.Run

Diese integrierten AsAsyncAction- und AsAsyncOperation-Erweiterungsmethoden sind bestens für einfache Umwandlungen von Task in IAsyncAction und von Task<TResult> in IAsyncOperation<TResult> geeignet. Aber wie steht es mit anspruchsvolleren Umwandlungen?

System.Runtime.WindowsRuntime.dll enthält einen weiteren Typ, der eine größere Flexibilität bietet: AsyncInfo im System.Runtime.InteropServices.WindowsRuntime-Namespace. AsyncInfo stellt vier Überladungen einer statischen Run-Methode bereit, eine für jede der vier asynchronen WinRT-Schnittstellen:

 // in System.Runtime.WindowsRuntime.dll
public static class AsyncInfo 
{
    // No results, no progress
    public static IAsyncAction Run(
        Func<CancellationToken, 
             Task> taskProvider); 

    // No results, with progress
    public static IAsyncActionWithProgress<TProgress> Run<TProgress>(
        Func<CancellationToken, 
             IProgress<TProgress>, 
             Task> taskProvider);


    // With results, no progress
    public static IAsyncOperation<TResult> Run<TResult>(
        Func<CancellationToken, 
             Task<TResult>> taskProvider);

    // With results, with progress
    public static IAsyncOperationWithProgress<TResult, TProgress> Run<TResult, TProgress>(
        Func<CancellationToken, 
             IProgress<TProgress>, 
             Task<TResult>> taskProvider);
}

Die bereits untersuchten AsAsyncAction- und AsAsyncOperation-Methoden akzeptieren Task als Argument. Im Gegensatz hierzu akzeptieren Run-Methoden einen Funktionsdelegaten, der Task zurückgibt. Dieser Unterschied zwischen Task und Func<…,Task> reicht für die zusätzliche Flexibilität aus, die wir für weitergehende Vorgänge benötigen.

Natürlich können Sie AsAsyncAction und AsAsyncOperation auch als einfache Hilfsmethoden zur Unterstützung der erweiterten AsyncInfo.Run betrachten:

 public static IAsyncAction AsAsyncAction(
    this Task source)
{
    return AsyncInfo.Run(_ => source);
}

public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
    this Task<TResult> source)
{
    return AsyncInfo.Run(_ => source);
}

Auch wenn dies nicht der exakten Implementierung in .NET 4.5 entspricht, ist das Verhalten funktional gesehen identisch. Der Unterschied besteht daher eher zwischen grundlegender und erweiterter Unterstützung. Verwenden Sie bei einfachen Fällen AsAsyncAction und AsAsyncOperation, bei weitergehenden Fällen bietet AsyncInfo.Run dagegen Vorteile.

Abbruch

Mit AsyncInfo.Run werden Abbrüche mit asynchronen WinRT-Methoden unterstützt.

Bleiben wir bei unserem oben verwendeten Downloadbeispiel. Angenommen es ist eine weitere DownloadStringAsyncInternal-Überladung vorhanden, die CancellationToken akzeptiert:

 internal static Task<string> DownloadStringAsyncInternal(
    string url, CancellationToken cancellationToken);

CancellationToken ist ein .NET Framework-Typ und unterstützt kooperative Abbrüche auf eine zusammensetzbare Weise. Sie können ein einzelnes Token an eine beliebige Anzahl von Methodenaufrufen übergeben. Wenn dieses Token den Abbruch (über die CancellationTokenSource, die das Token erstellt hat) anfordert, ist die Abbruchanforderung für sämtliche dieser Vorgänge sichtbar. Dieser Ansatz unterscheidet sich etwas vom Ansatz, der durch die asynchrone WinRT-Methode verwendet wird, bei dem jedes einzelne IAsyncInfo eine eigene Cancel-Methode verfügbar macht. Wie können wir unter diesen Voraussetzungen also für einen Aufruf von Cancel auf der IAsyncOperation<string> erreichen, dass der Abbruch über ein an DownloadStringAsyncInternal übergebenes CancellationToken angefordert wird? AsAsyncOperation kann in diesem Fall nicht verwendet werden:

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
     return DownloadStringAsyncInternal(uri, … /* what goes here?? */)
            .AsAsyncOperation();
}

Damit die Instanz weiß, wann der Abbruch der IAsyncOperation<string> angefordert wird, müsste diese Instanz einen Listener darüber informieren, dass Ihre Cancel-Methode aufgerufen wurde. Dies wäre beispielsweise durch eine Abbruchanforderung eines CancellationToken möglich, das dann an DownloadStringAsyncInternal übergeben würde. Allerdings erhalten wir die Task<string> , für die AsAsyncOperation aufgerufen werden soll, erst, wenn DownloadStringAsyncInternal bereits aufgerufen wurde. An diesem Punkt wäre die Verwendung genau dieses CancellationToken bereits erforderlich gewesen, das von AsAsyncOperation abgerufen werden soll.

Es gibt verschiedene Möglichkeiten, dieses Problem anzugehen, etwa durch die von AsyncInfo.Run bereitgestellte Lösung. Die Run-Methode ist für das Erstellen der IAsyncOperation<string> verantwortlich. Es erstellt diese Instanz, um den Abbruch eines CancellationToken anzufordern, das beim Aufruf der Cancel-Methode des asynchronen Vorgangs ebenfalls erstellt wird. Dieses Token wird anschließend übergeben, wenn der vom Benutzer bereitgestellte und an Run übergebene Delegat aufgerufen wird. So kann der zuvor erörterte Kreislauf vermieden werden:

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(cancellationToken => 
        DownloadStringAsyncInternal(uri, cancellationToken));
}

Lambdas und anonyme Methoden

AsyncInfo.Run vereinfacht die Verwendung von Lambda-Funktionen und anonymen Methoden zur Implementierung asynchroner WinRT-Methoden.

Wenn beispielsweise die DownloadStringAsyncInternal-Methode noch nicht vorhanden ist, könnten sie und DownloadStringAsync auf folgende Weise implementiert werden:

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(delegate(CancellationToken cancellationToken)
    {
        return DownloadStringAsyncInternal(uri, cancellationToken));
    });
}

private static async Task<string> DownloadStringAsyncInternal(
    string uri, CancellationToken cancellationToken)
{
    var response = await new HttpClient().GetAsync(
        uri, cancellationToken);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

Durch Verwendung der C#- und Visual Basic-Unterstützung für das Erstellen asynchroner anonymer Methoden, kann die Implementierung vereinfacht werden, indem wir diese beiden Methoden zu einer zusammenfassen:

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(async delegate(CancellationToken cancellationToken)
    {
        var response = await new HttpClient().GetAsync(
            uri, cancellationToken);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    });
}    

Fortschritt

AsyncInfo.Run stellt außerdem Unterstützung für Fortschrittsberichte durch asynchrone WinRT-Methoden bereit.

Stellen Sie sich vor, unsere DownloadStringAsync-Methode gäbe IAsyncOperationWithProgress<string,int> statt IAsyncOperation<string> zurück:

 public static IAsyncOperationWithProgress<string,int> DownloadStringAsync(string uri);

DownloadStringAsync kann jetzt Fortschrittsberichte mit integralen Daten bereitstellen. So können Consumer einen Delegaten als Progress-Eigenschaft der Schnittstelle festlegen, die dem Empfang von Benachrichtigungen zu Fortschrittsänderungen dient.

AsyncInfo.Run stellt außerdem eine Überladung bereit, die ein Func<CancellationToken,IProgress<TProgress>,Task<TResult>> akzeptiert. So wie AsyncInfo.Run ein CancellationToken in einen Delegaten übergibt, das bei Aufruf der Cancel-Methode einen Abbruch anfordert, kann auch eine IProgress<TProgress> -Instanz übergeben werden, deren Report-Methode Aufrufe des Progress-Delegaten des Consumer auslöst. Wenn unser obiges Beispiel z. B. so verändert wird, dass zu Beginn ein Fortschritt von 0 %, nach Erhalt der Antwort ein Fortschritt von 50 % und nach dem Parsen der Antwort in eine Zeichenfolge ein Fortschritt von 100 % ausgegeben wird, kann dies wie folgt aussehen:

 public static IAsyncOperationWithProgress<string,int> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(async delegate(
        CancellationToken cancellationToken, IProgress<int> progress)
    {
        progress.Report(0);
        try
        {
            var response = await new HttpClient().GetAsync(uri, cancellationToken);
            progress.Report(50);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        finally { progress.Report(100); }
    });
}

Tiefergehende Einblicke

Untersuchen wir nun eine Implementierung von AsAsyncOperation und AsyncInfo.Run, um dies zu veranschaulichen. Hierbei handelt es sich nicht um dieselben Implementierungen, wie in .NET 4.5, die deutlich stabiler sind. Es handelt sich dabei eher um Annäherungen, die die Abläufe verdeutlichen und nicht für den praktischen Einsatz vorgesehen sind.

AsAsyncOperation

Die AsAsyncOperation-Methode nimmt ein Task<TResult> und gibt ein IAsyncOperation<TResult> zurück:

 public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
    this Task<TResult> source);

Zur Implementierung dieser Methode muss ein Typ erstellt werden, der IAsyncOperation<TResult> implementiert und die bereitgestellte Aufgabe umschließt.

 public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
    this Task<TResult> source)
{
    return new TaskAsAsyncOperationAdapter<TResult>(source);
}

internal class TaskAsAsyncOperationAdapter<TResult> : IAsyncOperation<TResult>
{
    private readonly Task<TResult> m_task;

    public TaskAsAsyncOperationAdapter(Task<TResult> task) { m_task = task; }

    ...
}

Jede Implementierung einer Schnittstellenmethode für diesen Typ delegiert Funktionen der umschlossenen Aufgabe. Beginnen wir mit den einfachsten Membern:

Zuerst löscht die IAsyncInfo.Close-Methode alle Ressourcen, die vom abgeschlossenen asynchronen Vorgang verwendet wurden. Da keine entsprechenden Ressourcen vorhanden sind (unser Objekt schließt Aufgaben nur ein), ist unsere Implementierung leer:

 public void Close() { /* NOP */ }

Mit der IAsyncInfo.Cancel-Methode kann ein Consumer des asynchronen Vorgangs den Abbruch anfordern. Hierbei handelt es sich lediglich um eine Anforderung. Der Abbruch wird nicht erzwungen. CancellationTokenSource wird verwendet, um das Auftreten der Anforderung zu speichern:

 private readonly CancellationTokenSource m_canceler =
    new CancellationTokenSource();

public void Cancel() { m_canceler.Cancel(); }

Die IAsyncInfo.Status-Eigenschaft gibt einen AsyncStatus zurück, um den aktuellen Status des Vorgangs in Bezug auf den asynchronen Lebenszyklus darzustellen. Hierbei ist einer der folgenden vier Werte möglich: Started, Completed, Error oder Canceled. In den meisten Fällen können wir die Status-Eigenschaft des zugrunde liegenden Tasks einfach delegieren und vom zurückgegebenen TaskStatus zum erforderlichen AsyncStatus verweisen:

Von TaskStatus

Zu AsyncStatus

RanToCompletion

Completed

Faulted

Error

Canceled

Canceled

Alle anderen Werte und Abbruch angefordert

Canceled

Alle anderen Werte und Abbruch nicht angefordert

Started

Wenn Task noch nicht abgeschlossen ist und der Abbruch angefordert wurde, muss Canceled anstelle von Started zurückgegeben werden. Das heißt, obwohl TaskStatus.Canceled nur ein abschließender Zustand ist, kann AsyncStatus.Canceled entweder ein abschließender oder ein nicht abschließender Zustand sein. So kann eine IAsyncInfo in einem Canceled-Status enden oder eine IAsyncInfo im Canceled-Status entweder in AsyncStatus.Completed oder AsyncStatus.Error umgewandelt werden.

 public AsyncStatus Status
{
    get
    {
        switch (m_task.Status)
        {
            case TaskStatus.RanToCompletion: 
                return AsyncStatus.Completed;
            case TaskStatus.Faulted: 
                return AsyncStatus.Error;
            case TaskStatus.Canceled: 
                return AsyncStatus.Canceled;
            default: 
                return m_canceler.IsCancellationRequested ? 
                    AsyncStatus.Canceled : 
                    AsyncStatus.Started;
        }
    }
}

Die IAsyncInfo.Id-Eigenschaft gibt einen UInt32-Bezeichner für den Vorgang zurück. Da Task bereits selbst einen solchen Bezeichner (als Int32) verfügbar macht, können wir diese Eigenschaft ganz einfach implementieren, indem wir durch die zugrunde liegende Task-Eigenschaft delegieren:

 public uint Id { get { return (uint)m_task.Id; } }

WinRT definiert die IAsyncInfo.ErrorCode-Eigenschaft so, dass diese ein HResult zurückgibt. Die CLR weist das HResult der WinRT intern einer .NET-Exception zu, und stellt uns diese so durch die verwaltete Projektion bereit. Task selbst stellt eine Exception-Eigenschaft bereit damit wir direkt zu dieser delegieren können: Wenn Task im Faulted-Status beendet wird, wird die erste Ausnahme zurückgegeben (ein Task könnte auch aufgrund mehrerer Ausnahmen fehlschlagen, z. B. wenn Task von Task.WhenAll zurückgegeben wird), ansonsten wird null zurückgegeben:

 public Exception ErrorCode
{
    get { return m_task.IsFaulted ? m_task.Exception.InnerException : null; }
}

Hiermit ist die Implementierung von IAsyncInfo abgeschlossen. Nun müssen wir die beiden zusätzlichen Member implementieren, die von IAsyncOperation<TResult> bereitgestellt werden: GetResults und Completed.

Consumer rufen GetResults auf, nachdem der Vorgang erfolgreich abgeschlossen wurde oder nachdem eine Ausnahme aufgetreten ist. Im ersten Fall werden die berechneten Ergebnisse zurückgegeben, im zweiten Fall wird die relevante Ausnahme ausgelöst. Wenn der Vorgang mit Canceled beendet wurde oder noch nicht abgeschlossen ist, ist der Aufruf von GetResults nicht zulässig. GetResults kann wie folgt implementiert werden und beruht dabei auf der GetResult-Methode des Task-Awaiters, die bei einem Erfolg das Ergebnis zurückgibt oder die richtige Ausnahme verteilt, wenn der Task als Faulted beendet wurde.

 public TResult GetResults()
{
    switch (m_task.Status)
    {
        case TaskStatus.RanToCompletion:
        case TaskStatus.Faulted:
            return m_task.GetAwaiter().GetResult();
        default:
            throw new InvalidOperationException("Invalid GetResults call.");
    }
}

Zuletzt ist noch die Completed-Eigenschaft vorhanden. Completed stellt einen Delegaten dar, der beim Abschluss des Vorgangs aufgerufen werden sollte. Wenn der Vorgang beim Festlegen von Completed bereits abgeschlossen ist, muss der bereitgestellte Delegat umgehend aufgerufen oder geplant werden. Außerdem kann ein Consumer die Eigenschaft nur einmalig festlegen (wird dies mehrfach versucht, werden Ausnahmen ausgelöst). Wenn der Vorgang abgeschlossen ist, müssen Implementierungen den Verweis zum Delegaten verwerfen, um einen Arbeitsspeicherverlust zu vermeiden. Wir können auf die ContinueWith-Methode von Task zurückgreifen, um weite Teile dieses Verhaltens zu implementieren, da ContinueWith jedoch mehrfach für dieselbe Task-Instanz angewendet werden kann, muss das restriktivere „set once“-Verhalten manuell implementiert werden:

 private AsyncOperationCompletedHandler<TResult> m_handler;
private int m_handlerSet;

public AsyncOperationCompletedHandler<TResult> Completed
{
    get { return m_handler; }
    set
    {
        if (value == null) 
            throw new ArgumentNullException("value");
        if (Interlocked.CompareExchange(ref m_handlerSet, 1, 0) != 0)
            throw new InvalidOperationException("Handler already set.");

        m_handler = value;

        var sc = SynchronizationContext.Current;
        m_task.ContinueWith(delegate {
            var handler = m_handler;
            m_handler = null;
            if (sc == null)
                handler(this, this.Status);
            else
                sc.Post(delegate { handler(this, this.Status); }, null);
       }, CancellationToken.None, 
          TaskContinuationOptions.ExecuteSynchronously, 
          TaskScheduler.Default);
    }
}

Damit ist unsere Implementierung von AsAsyncOperation vollständig, und wir können alle Methoden, die Task<TResult> zurückgeben, als IAsyncOperation<TResult> -Methode verwenden.

AsyncInfo.Run

Wie sieht es nun mit der erweiterten AsyncInfo.Run aus?

 public static IAsyncOperation<TResult> Run<TResult>(
        Func<CancellationToken,Task<TResult>> taskProvider);

Um diese Run-Überladung zu unterstützen, können wir den soeben erstellten TaskAsAsyncOperationAdapter<TResult> -Typ verwenden, bei dem alle vorhandenen Implementierungen intakt sind. Dieser muss lediglich mit der Fähigkeit erweitert werden, auch mit Func<CancellationToken,Task<TResult>> statt nur mit Task<TResult> zu funktionieren. Wenn ein solcher Delegat bereitgestellt wird, kann dieser ganz einfach synchron aufgerufen werden, indem die zuvor definierte m_canceler CancellationTokenSource übergeben und die zurückgegebene Aufgabe gespeichert wird:

 internal class TaskAsAsyncOperationAdapter<TResult> : IAsyncOperation<TResult>
{
    private readonly Task<TResult> m_task;

    public TaskAsAsyncOperationAdapter(Task<TResult> task) { m_task = task; }

    public TaskAsAsyncOperationAdapter(
        Func<CancellationToken,Task<TResult>> func)
    {
        m_task = func(m_canceler.Token);
    }
    ...
}

public static class AsyncInfo
{
    public static IAsyncOperation<TResult> Run<TResult>(
        Func<CancellationToken, Task<TResult>> taskProvider)
    {
        return new TaskAsAsyncOperationAdapter<TResult>(taskProvider);
    }
    …
}

Diese Implementierung zeigt, dass hier keine Hexerei am Werk ist. AsAsyncAction, AsAsyncOperation und AsyncInfo.Run sind lediglich Hilfsimplementierungen, durch die Sie diese Standardbausteine nicht selbst verfassen müssen.

Fazit

Sie sollten nun eine gute Vorstellung davon haben, wofür AsAsyncAction, AsAsyncOperation und AsyncInfo.Run eingesetzt werden: Sie vereinfachen die Bereitstellung von Task oder Task<TResult> als IAsyncAction, IAsyncOperation<TResult> , IAsyncActionWithProgress<TProgress> oder IAsyncOperationWithProgress<TResult,TProgress> . In Kombination mit den Schlüsselwörtern async und await in C# und Visual Basic vereinfacht dies die Implementierung von neuen asynchronen WinRT-Vorgängen in verwaltetem Code erheblich.

So lange die verfügbar gemachten Funktionen als Task oder Task<TResult> verfügbar sind, sollten Sie Konvertierungen durch diese integrierten Funktionen ausführen lassen, statt die asynchronen WinRT-Schnittstellen manuell zu implementieren. Wenn die Funktionen, die Sie verfügbar machen möchten, noch nicht als Task oder Task<TResult> bereit stehen, sollten Sie diese zunächst als Task oder Task<TResult> verfügbar machen und anschließend auf die integrierten Konvertierungen zurückgreifen. Es ist eine schwierige Aufgabe, die gesamte Semantik für die Implementierung asynchroner WinRT-Schnittstellen korrekt einzusetzen – daher wurden Ihnen diese Konvertierungen für diese Aufgabe bereitgestellt. Eine ähnliche Unterstützung bieten wir Ihnen auch für die Implementierung asynchroner WinRT-Vorgänge in C++, gleichgültig, ob durch die AsyncBase-Basisklasse in der Windows Runtime Library oder durch die create_async Funktion in der Parallel Pattern Library.

Viel Spaß bei der asynchronen Programmierung!

– Stephen Toub, Visual Studio