Delphi - Выполнение асинхронных вызовов WinRT API с использованием TTask.Future

В связи с этим вопросом я пытаюсь реализовать процедуру, которая использует WinRT API для установки обоев рабочего стола. Подражать await функциональность в C#, я использую TTask.Future ( ссылка), как указано здесь и здесь.

Моя реализация выглядит так:

class procedure TUtilityWin.SetWallpaper(AFileName: String);
var
  lStorageFile: IStorageFile;
  liao_storagefile: IAsyncOperation_1__IStorageFile;
  lFutureTask: IFuture<IAsyncOperation_1__IStorageFile>;
begin
  //WinRT Implementation
  if TUserProfile_UserProfilePersonalizationSettings.IsSupported then
  begin
    lFutureTask:=TTask.Future<IAsyncOperation_1__IStorageFile>(
                          function: IAsyncOperation_1__IStorageFile
                          begin
                            Result:=TStorageFile.GetFileFromPathAsync(HSTRING(AFileName));
                          end);
    liao_storagefile:=lFutureTask.Value;
    lStorageFile:=liao_storagefile.GetResults;
    TUserProfile_UserProfilePersonalizationSettings.Current.TrySetWallpaperImageAsync(lStorageFile);
  end;
end;

В моем понимании, когда я пытаюсь получить lFutureTask.Valueприложение приостанавливает текущий поток до тех пор, пока lFutureTask не будет завершен (если это еще не сделано), а затем предоставит значение. Однако, когда я запускаю приложение, я получаю сообщение об ошибке: EOleException with message 'A method was called at an unexpected time', Разрыв на этой линии: lStorageFile:=liao_storagefile.GetResults;

Я новичок в TTask, а также в WinRT API - так что я уверен, что мне здесь чего-то не хватает. Буду признателен за любые указания на то, что может вызвать это или что я мог бы сделать по-другому, чтобы исправить это. Заранее спасибо.

1 ответ

Следующее (обработка асинхронного вызова WebAuthenticationCoreManager.FindAccountProviderAsync) работает для меня, хотя он настолько уродлив, насколько это возможно по сравнению с async/await в.Net:

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Winapi.Winrt,
  System.Win.WinRT,
  Winapi.Security.Credentials,
  Winapi.Security,
  Winapi.Foundation.Types

...

type
  TCompleteOperationCompleted_IWebAccountProvider = reference to procedure (Value : IWebAccountProvider);
  TCompleteOperationError_IWebAccountProvider   = reference to procedure ();

  TAsyncOperationCompletedHandler_1__IWebAccountProvider = class (TInterfacedObject,
                                                                  AsyncOperationCompletedHandler_1__IWebAccountProvider_Delegate_Base,
                                                                  AsyncOperationCompletedHandler_1__IWebAccountProvider)
  private
    fOnCompleted : TCompleteOperationCompleted_IWebAccountProvider;
    fOnError : TCompleteOperationError_IWebAccountProvider;
  protected
    procedure Invoke(asyncInfo: IAsyncOperation_1__IWebAccountProvider; asyncStatus: AsyncStatus); safecall;
  public
    constructor Create(OnCompleted : TCompleteOperationCompleted_IWebAccountProvider;
                       OnError : TCompleteOperationError_IWebAccountProvider = nil);
  end;

constructor TAsyncOperationCompletedHandler_1__IWebAccountProvider.Create(
  OnCompleted: TCompleteOperationCompleted_IWebAccountProvider;
  OnError: TCompleteOperationError_IWebAccountProvider);
begin
  fOnCompleted := OnCompleted;
  fOnError   := OnError;
end;

procedure TAsyncOperationCompletedHandler_1__IWebAccountProvider.Invoke(
  asyncInfo: IAsyncOperation_1__IWebAccountProvider; asyncStatus: AsyncStatus);
begin
  case asyncStatus of
    Winapi.Foundation.Types.AsyncStatus.Completed : if Assigned(fOnCompleted) then fOnCompleted(asyncInfo.GetResults());
    Winapi.Foundation.Types.AsyncStatus.Error : if Assigned(fOnError) then fOnError();
    else ;//todo
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
const
  DefaultProviderId = 'https://login.windows.local';
  MicrosoftProviderId = 'https://login.microsoft.com';
var
  account: IAsyncOperation_1__IWebAccountProvider;
  webAccount: IWebAccountProvider;
begin
  account := TAuthentication_Web_Core_WebAuthenticationCoreManager.FindAccountProviderAsync(TWindowsString.Create(MicrosoftProviderId{DefaultProviderId}));

  account.Completed := TAsyncOperationCompletedHandler_1__IWebAccountProvider.Create(
                       procedure(Value : IWebAccountProvider)
                       begin
                         ShowMessage('Async operation completed');
                         //todo
                       end,
                       nil
                       );
end;

Я взглянул на документы Delphi, связанные в вашем вопросе, и AFAICT оба ITask а также IFuture представляют только те методы, которые выполняются в отдельном потоке (то, что я называю "делегировать задачи"). Кажется, что нет никакой поддержки для асинхронных задач (то, что я называю "Задачи обещания"). IAsyncOperation<T> является типом WinRT, представляющим асинхронную задачу, поэтому у вас возникли проблемы В частности, похоже, что Delphi не поддерживает регистрацию продолжений в их "заданиях".

Таким образом, если только у Delphi нет поддержки для многопоточного Future / Promise, вам придется блокировать поток.

В настоящее время ваш код раскручивает многопоточную задачу, которая только запускает асинхронную операцию (GetFileFromPathAsync). Потоковая задача не ожидает завершения асинхронной операции (IAsyncOperation<T>.Completed), так что задача завершается сразу после начала операции, а затем вызывается внешний код GetResult когда операция еще не имеет результата, вызывая исключение.

Итак, чтобы это исправить, вам понадобится способ иметь блок задач, пока асинхронная операция не завершится. Поскольку типы WinRT являются чисто асинхронными (без поддержки синхронности), а типы Delphi являются чисто синхронными (без поддержки асинхронности), вам придется соединять их самостоятельно. Наилучшим решением, вероятно, является Delphi-эквивалент ManualResetEvent.

Примерно так должно работать:

class procedure TUtilityWin.SetWallpaper(AFileName: String);
var
  lStorageFile: IStorageFile;
  lFutureTask: IFuture<IStorageFile>;
begin
  if TUserProfile_UserProfilePersonalizationSettings.IsSupported then
  begin
    lFutureTask:=TTask.Future<IStorageFile>(
        function: IStorageFile
        var
          liao_storagefile: IAsyncOperation_1__IStorageFile;
          mre: ManualResetEvent;
          result: IStorageFile;
        begin
          mre:= // Create ManualResetEvent
          liao_storagefile:=TStorageFile.GetFileFromPathAsync(HSTRING(AFileName));
          liao_storagefile.Completed... // Add handler that will set `result` and then set the ManualResetEvent
          mre.Wait(); // Wait for the ManualResetEvent to be set
          Result:=result;
        end);
    liao_storagefile:=lFutureTask.Value;
    lStorageFile:=liao_storagefile.GetResults;
    TUserProfile_UserProfilePersonalizationSettings.Current.TrySetWallpaperImageAsync(lStorageFile);
  end;
end;

Извините, я не знаю Delphi, но, надеюсь, это даст вам общее направление.