ADO: Как выполнить запрос синхронно с возможностью отмены? - PullRequest
0 голосов
/ 08 ноября 2010

Сейчас у меня есть функция, которая запускает запрос, используя ADO, и возвращает набор записей :

Recordset Execute(Connection connection, String commandText)
{
   //[pseudo-code]
   Recordset rs = new Recordset();
   rs.CursorLocation = adUseClient;   
   rs.CursorType = adOpenForwardOnly;   
   rs.Open(commandText, connection, 
         adOpenForwardOnly, //CursorType; the default
         adLockReadOnly, //LockType
         adCmdText);       

   return rs;
}

И это нормально. Он работает синхронно и возвращает набор записей запроса.

Теперь я хочу подобную версию, которая показывает ProgressDialog , предоставляя пользователю возможность отменить длительный запрос:

Recordset Execute(HWND parenthWnd, String caption, Connection connection, String commandText)
{
   //[pseudo-code]

   //Construct a progressDialog and show it
   IProgressDialog pd = new ProgressDialog();
   pd.SetTitle(caption); //e.g. "Annual Funding Report"
   pd.SetCancelMsg("Please wait while the operation is cancelled");
   pd.StartProgressDialog(parenthWnd, null, PROGDLG_MODAL | PROGDLG_NOTIME | PROGDLG_NOMINIMIZE, null);
   pd.SetLine(1, "Querying server", False, null);
   try
   {
      //Query the server
      Recordset rs = new Recordset();
      rs.Open(commandText, connection, 
            adOpenForwardOnly, //CursorType
            adLockReadOnly, //LockType
            adCmdText | adAsyncExecute); 

      while (rs.State and (adStateConnecting+adStateExecuting+adStateFetching) <> 0) 
      {
         if pd.HasUserCancelled()
            throw new EUserCancelledOperationException();

         Sleep(100);
      };

   finally
   {
      //Hide and destroy the progress dialog     
      pd.StopProgressDialog();
      pd = null;
   }

   //Now we have our results for the client
   return rs;
}

Чтобы проверить, отменил ли пользователь операцию, нужно периодически запрашивать диалог прогресса, если пользователь нажал кнопку отмены:

pd.HasUserCancelled(); //returns true if user has clicked Cancel

Теперь я сталкиваюсь с тем, как периодически проверять, отменил ли пользователь (и знать, завершился ли запрос или произошла ошибка), и быть хорошим программистом и делать это без опроса.

Единственный способ узнать, что произошла ошибка, - это обработчик набора записей FetchCompleteEvent :

pError
Объект Error. Он описывает ошибку, которая произошла, если значением adStatus является adStatusErrorsOccurred; в противном случае он не установлен.

adStatus
Значение состояния EventStatusEnum. Когда вызывается это событие, для этого параметра устанавливается значение adStatusOK, если операция, вызвавшая событие, было успешным, или значение adStatusErrorsOccrogen, если операция завершилась неудачно.

Перед возвратом этого события установите для этого параметра значение adStatusUnwantedEvent, чтобы предотвратить последующие уведомления.

pRecordset
Объект Recordset. Объект, для которого были получены записи.

Так что это будет означать, что мне понадобится, чтобы моя функция создала вспомогательный объект, чтобы я мог иметь обработчик FetchComplete. Но тогда я должен предотвратить немедленное возвращение моей синхронной функции. И тогда мы попадаем в MsgWaitForSingleObject, который, как известно, трудно правильно использовать.

Так что я прошу помощи или законный код.


Я должен быть более точным: я ищу реализацию функции с сигнатурой этого метода:

Recordset ExecuteWithCancelOption(Connection connection, String commandText)

, который показывает диалог с кнопкой отмены.

Задача состоит в том, что теперь функция должна создавать все, что требуется для этого. Если это связано со скрытой формой, на которой есть таймер и т. Д. - хорошо.

Но я ищу синхронную функцию , которая отображает кнопку Отмена .

И функция будет почти (или точной) заменой для

Recordset Execute(Connection connection, String commandText)

Учитывая практические соображения по Windows, мне нужно было бы снабдить функцию дескриптором родительского окна, к которому она будет привязывать свой диалог:

Recordset ExecuteWithCancelOption(HWND parentHwnd, Connection connection, String commandText)

И учитывая, что эта функция будет использоваться повторно, я позволю вызывающей стороне предоставить текст, который будет отображаться:

Recordset ExecuteWithCancelOption(HWND parenthWnd, String caption, Connection connection, String commandText)

И, учитывая, что это обе функции класса в моем классе TADOHelper, я могу дать им одно и то же имя, и они будут перегрузками друг друга:

Recordset Execute(HWND parenthWnd, String caption, Connection connection, String commandText)

Я думаю, что на языках, отличных от Delphi, анонимные делегаты полезны. Но я все еще боюсь иметь дело с MsgWaitForMultipleObjects.

Ответы [ 3 ]

1 голос
/ 09 ноября 2010
  1. Информация о ходе выполнения и постепенная отмена запроса могут быть доступны не на каждом ядре базы данных.Им нужна поддержка базы данных, как на стороне сервера, так и на стороне клиента.Например, Oracle позволяет отменить запрос, но при этом не имеет информации «о ходе выполнения», но читает представление V $ SESSION_LONGOPS.Конечно, вы можете прервать сеанс, но он откатит его полностью, а не просто отменит выполнение запроса.
  2. Обычно, если база данных поддерживает такие функции, запрос выполняется в отдельном потоке, которыйбудем ждать результата.Таким образом, основной поток все еще может получать пользовательский ввод или считывать и отображать информацию о ходе выполнения (если не возвращено в каком-либо обратном вызове).Если пользователь отменяет запрос, то выполняется соответствующий вызов, чтобы остановить операцию, позволяя потоку запроса вернуться, обычно поток получит код состояния, который сообщит о том, что произошло.
  3. Помните о том, как ADO реализуетасинхронные операции: http://msdn.microsoft.com/en-us/library/ms681467(VS.85).aspx
  4. Также есть событие FetchProgress (), которое может помочь вам, если вы не хотите идти по пути (и даже тогда, если возможно, отменить запрос)
1 голос
/ 09 ноября 2010

Для того, чтобы графический интерфейс реагировал на нажатия кнопок, необходимо вернуть управление в цикл сообщений окна. Цикл while while (rs.State и (adStateConnecting + adStateExecuting + adStateFetching) <> 0) не возвращает управление обратно в цикл сообщений, блокируя таким образом графический интерфейс.


Ниже приведена выдержка из рабочего кода Delphi, который использует асинхронные запросы ADO. Этот код не допускает немодальной выборки данных, но гарантирует, что основная форма перерисовывается во время выборки данных, а также позволяет отменять запрос.

Асинхронное выполнение и выборка достигается установкой:
FOpeningDataSet.ExecuteOptions := [eoAsyncExecute, eoAsyncFetchNonBlocking]; выполнение запроса отменяется вызовом
DataSet.Recordset.Cancel;
в FetchProgress событие.

Любой TADODataSet должен быть открыт с помощью метода:

OpenDataSetInBackground(DataSourceData.DataSet as TADODataSet);

Код поддержки в основной форме:

procedure TOperatorForm.OpenDataSetInBackground(DataSet: TADODataSet);
begin
  if DataSet.Active then Exit;
  FOpeningDataSet := DataSet;

  if not FAsyncDataFetch then
  begin
    FOpeningDataSet.Open;
    Exit;
  end;

  FFetchCancel := False;
  FExecuteOptions := FOpeningDataSet.ExecuteOptions;
  FFetchProgress := FOpeningDataSet.OnFetchProgress;
  FFetchComplete := FOpeningDataSet.OnFetchComplete;
  FRecordsetCreate := FOpeningDataSet.OnRecordsetCreate;
  FAfterScroll := FOpeningDataSet.AfterScroll;
  FOpeningDataSet.ExecuteOptions := [eoAsyncExecute, eoAsyncFetchNonBlocking];
  FOpeningDataSet.OnFetchProgress := DataSetFetchProgress;
  FOpeningDataSet.OnFetchComplete := DataSetFetchComplete;
  FOpeningDataSet.OnRecordsetCreate := DataSetRecordsetCreate;
  FOpeningDataSet.AfterScroll := DataSetAfterScroll;
  FOpeningDataSet.CursorLocation := clUseClient;
  FOpeningDataSet.DisableControls;
  try
    DataSetProgressForm.Left := Left + (Width - DataSetProgressForm.Width) div 2;
    DataSetProgressForm.Top := Top + (Height - DataSetProgressForm.Height) div 2;
    DataSetProgressForm.cxButton1.OnClick := DataSetProgressClick;
    DataSetProgressForm.cxButton1.Visible := FShowProgressCancelButton;

    FOpeningDataSet.Open;
    DataSetProgressForm.ShowModal;

  finally
    FOpeningDataSet.EnableControls;
    FOpeningDataSet.ExecuteOptions := FExecuteOptions;
    FOpeningDataSet.OnFetchProgress := FFetchProgress;
    FOpeningDataSet.OnFetchComplete := FFetchComplete;
    FOpeningDataSet.OnRecordsetCreate := FRecordsetCreate;
    FOpeningDataSet.AfterScroll := FAfterScroll;
  end;
end;

procedure TOperatorForm.DataSetProgressClick(Sender: TObject);
begin
  FFetchCancel := True;
end;

procedure TOperatorForm.DataSetFetchProgress(DataSet: TCustomADODataSet; Progress, MaxProgress: Integer; var EventStatus: TEventStatus);
begin
  if FFetchCancel then
    DataSet.Recordset.Cancel;
end;

procedure TOperatorForm.DataSetFetchComplete(DataSet: TCustomADODataSet; const Error: Error; var EventStatus: TEventStatus);
begin
  PostMessage(DataSetProgressForm.Handle, WM_CLOSE, 0, 0);
  MessageBeep(MB_ICONEXCLAMATION);
end;

procedure TOperatorForm.DataSetFetchComplete(DataSet: TCustomADODataSet; const Error: Error; var EventStatus: TEventStatus);
begin
  PostMessage(DataSetProgressForm.Handle, WM_CLOSE, 0, 0);
  MessageBeep(MB_ICONEXCLAMATION);
end;

procedure TOperatorForm.DataSetRecordsetCreate(DataSet: TCustomADODataSet; const Recordset: _Recordset);
begin
  if Assigned(FRecordsetCreate) then FRecordsetCreate(DataSet, Recordset);
end;

procedure TOperatorForm.DataSetAfterScroll(DataSet: TDataSet);
begin
  // From TBetterADODataSet 4.04
  // Ole Willy Tuv's fix 03-10-00 for missing first record
  with TADODataSet(DataSet) do
  begin
    if (eoAsyncFetchNonBlocking in ExecuteOptions) and
       (Bof or Eof) and
       (CursorLocation = clUseClient) and
       (stFetching in RecordSetState) then
    begin
      if Recordset.RecordCount > 0 then
        if Bof then
          Recordset.MoveFirst
        else if Eof then
          Recordset.MoveLast;
      CursorPosChanged;
      Resync([]);
    end;
  end;
  if Assigned(FAfterScroll) then
    FAfterScroll(DataSet);
end;

Форма выполнения:

unit uDataSetProgressForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, ExtCtrls, StdCtrls;

type
  TDataSetProgressForm = class(TForm)
    AnimateProgress: TAnimate;
    Label1: TLabel;
    Bevel1: TBevel;
    Bevel2: TBevel;
    Button1: TButton;
    Shape1: TShape;
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormHide(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  DataSetProgressForm: TDataSetProgressForm;

implementation

{$R *.dfm}
{$R servertimeout.res} // contains IDR_SERVAVI animation resource

procedure TDataSetProgressForm.FormCreate(Sender: TObject);
begin
  AnimateProgress.ResName := 'IDR_SERVAVI';
end;

procedure TDataSetProgressForm.FormShow(Sender: TObject);
begin
  AnimateProgress.Active := True;
end;

procedure TDataSetProgressForm.FormHide(Sender: TObject);
begin
  AnimateProgress.Active := False;
end;

end.

и dfm

object DataSetProgressForm: TDataSetProgressForm
  Left = 590
  Top = 497
  BorderStyle = bsNone
  ClientHeight = 104
  ClientWidth = 205
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  FormStyle = fsStayOnTop
  OldCreateOrder = False
  Position = poDefaultSizeOnly
  OnCreate = FormCreate
  OnHide = FormHide
  OnShow = FormShow
  DesignSize = (
    205
    104)
  PixelsPerInch = 96
  TextHeight = 13
  object Bevel1: TBevel
    Left = 0
    Top = 0
    Width = 205
    Height = 104
    Align = alClient
    Style = bsRaised
  end
  object Bevel2: TBevel
    Left = 12
    Top = 12
    Width = 181
    Height = 80
    Anchors = [akLeft, akTop, akRight, akBottom]
  end
  object Shape1: TShape
    Left = 1
    Top = 1
    Width = 203
    Height = 102
    Anchors = [akLeft, akTop, akRight, akBottom]
    Brush.Style = bsClear
    Pen.Color = clWindowFrame
  end
  object AnimateProgress: TAnimate
    Left = 25
    Top = 23
    Width = 32
    Height = 32
  end
  object Label1: TLabel
    Left = 70
    Top = 31
    Width = 106
    Height = 17
    Hint = 'Selecting data...'
    Caption = 'Selecting data...'
    TabOrder = 1
  end
  object Button1: TButton
    Left = 63
    Top = 64
    Width = 80
    Height = 23
    Caption = 'Cancel'
    Default = True
    TabOrder = 2
  end
end
0 голосов
/ 08 ноября 2010

Если это Delphi, вы можете добавить компонент TTimer и использовать его для проверки, имеет ли значение HasUserCancelled значение True. У меня нет Delphi передо мной, поэтому мне придется опубликовать пример позже.

Edit:

Вот пример события TTimer OnTimer, которое проверяет текущее время и время активности, чтобы решить, что делать с формами, если программа была оставлена ​​«Вверх»:

procedure TForm_Main.Timer1Timer(Sender: TObject);
begin
  // return to opening screen if no activity for a while:
  if Now - LastActivity > TimeOut
  then
    begin
      Form_Select.SBtn_Defendant.Down:= False;
      Form_Select.SBtn_Officer.Down:= False;
      Form_Select.SBtn_Attorney.Down:= False;
      Form_Main.ModalResult:= mrCancel;
      Exit;
    end;
  Form_Main.Caption:= FormatDateTime('dddd   mmmm d, yyyy  h:nn:ss AM/PM',    Now);
end;
...