IceCold
IceCold

Reputation: 21124

Downloading files in background without blocking the gui

I have a list of records. Each record has an

URL:= string

field. Via GUI the user can edit revords or even delete records (rows) entirely. I would like to download in background in a thread all online files pointed by the URL field. Of course, I don't want to lock the GUI when the thread downloads the files. So, how do I make sure the program/user cannot access the record processed currently by the thread?

Upvotes: 1

Views: 859

Answers (3)

nil
nil

Reputation: 1328

Following is based on the solution of Tom Brunberg using a record. Idea that the record will begin downloading via a TThread (the implementation of the download itself is out of the question as I understand). It might be a bit rough, let me know if there are severe mistakes for example in handling the thread.

While downloading, the data is not accessible, I decided to throw an exception when accessed, but that's up to implementation details of the GUI. property IsDownLoading: Boolean can be used to e.g. disable controls that would normally make the data accessible, too.

Still, the URL can be changed at all times by user, terminating the current download if in process.

A TDownloadThread should only be present while needed. If there are lots of these records, this should reduce unneeded resources.

unit Unit1;

interface

uses
  System.Classes, System.SysUtils;

type
  TDownLoadThread = class(TThread)
  private
    FURL: string;
    FData: Variant;
    procedure SetURL(const Value: string);
  protected
    procedure Execute; override;    
  public
    property Data: Variant read FData;
    property URL: string read FURL write SetURL;
  end;

  TDownLoadRecord = record
  private
    FData: Variant;
    FURL: string;
    FDownLoadThread: TDownLoadThread;
    procedure DownLoadThreadTerminate(Sender: TObject);
    function GetIsDownLoading: Boolean;
    procedure SetURL(const Value: string);
    procedure URLChanged;
    function GetData: Variant;
  public
    property Data: Variant read GetData;  
    property URL: string read FURL write SetURL;
    property IsDownLoading: Boolean read GetIsDownLoading;
  end;

implementation

{ TDownLoadRecord }

procedure TDownLoadRecord.DownLoadThreadTerminate(Sender: TObject);
begin
  FData := FDownLoadThread.Data;
  FDownLoadThread := nil;
end;

function TDownLoadRecord.GetData: Variant;
begin
  if not IsDownLoading then
    Result := FData
  else
    raise Exception.Create('Still downloading');
end;

function TDownLoadRecord.GetIsDownLoading: Boolean;
begin
  Result := (FDownLoadThread <> nil) and not FDownLoadThread.Finished;
end;

procedure TDownLoadRecord.SetURL(const Value: string);
begin
  if FURL <> Value then
  begin
    FURL := Value;
    URLChanged;
  end;
end;

procedure TDownLoadRecord.URLChanged;
begin
  if FURL <> '' then
  begin
    if FDownLoadThread <> nil then
      TDownLoadThread.Create(True)
    else
      if not FDownLoadThread.CheckTerminated then
        FDownLoadThread.Terminate;
    FDownLoadThread.URL := FURL;
    FDownLoadThread.FreeOnTerminate := True;
    FDownLoadThread.OnTerminate := DownLoadThreadTerminate;
    FDownLoadThread.Start;
  end;
end;

{ TDownLoadThread }

procedure TDownLoadThread.Execute;
begin
  // Download
end;

procedure TDownLoadThread.SetURL(const Value: string);
begin
  FURL := Value;
end;

end.

Upvotes: 2

Tom Brunberg
Tom Brunberg

Reputation: 21033

So, how do I make sure the program/user cannot access the record processed currently by the thread?

In "modern" (sine Delphi 2006 I think) records you can use properties with getters and setters just as with classes. In the setter you can prevent or allow changes to the underlying field.

A naive example:

type
  TMyRecord = record
  private
    FURL: string;
    FDownloading: boolean;
    procedure SetTheURL(NewURL: string);
  public
    property TheURL: string read FURL write SetTheURL;
    procedure DownLoad;
  end;

procedure TMyRecord.SetTheURL(NewURL: string);
begin
  if not FDownloading then
    FURL := NewURL;
  else
    // signal inability to change
end;

procedure TMyRecord.DownLoad;
begin
  FDownLoading := True;
  // hand the downloading task to a thread
end;

Here's the documentation under Records(advanced)

Upvotes: 3

coding Bott
coding Bott

Reputation: 4357

I really like to use BITS for downloads. Access from Delphi is easy. In BITS your define jobs, which are downloaded in background. When ready you can call a EXE, you can poll in the idle loop for the result or you can get an event.

Here is a samples - you will need the jedi lib! That sample needs to be extended for production quality (error handling, logging, job name)!

unit uc_DownloadBits;

interface

uses
  ExtActns;

type
  TDownloadBits = class
  public
    class procedure DownloadForground(ziel, downloadurl: WideString; DownloadFeedback:TDownloadProgressEvent);
    class procedure DownloadBackground(ziel, downloadurl, ExeName, Params: WideString);
    class procedure CompleteJob(JobId: WideString);
  end;

implementation

uses
  ComObj, ActiveX, SysUtils,
  JwaBits, JwaBits1_5, Windows;

{ TDownloadBits }

class procedure TDownloadBits.CompleteJob(JobId: WideString);
var
  bi: IBackgroundCopyManager;
  job: IBackgroundCopyJob;
  g: TGuid;
begin
  bi:=CreateComObject(CLSID_BackgroundCopyManager) as IBackgroundCopyManager;
  g:=StringToGUID(jobid);
  bi.GetJob(g,job);
  job.Complete();
end;

class procedure TDownloadBits.DownloadBackground(ziel, downloadurl,
  ExeName, Params: WideString);

var
  bi: IBackgroundCopyManager;
  job: IBackgroundCopyJob;
  job2: IBackgroundCopyJob2;
  jobId: TGUID;
  r: HRESULT;

begin
  bi:=CreateComObject(CLSID_BackgroundCopyManager) as IBackgroundCopyManager;
  r:=bi.CreateJob('Updatedownload', BG_JOB_TYPE_DOWNLOAD, JobId, job);
  if not Succeeded(r) then
    raise Exception.Create('Create Job Failed');
  r:=Job.AddFile(PWideChar(downloadurl), PWideChar(ziel));
  if not Succeeded(r) then
    raise Exception.Create('Add File Failed');
  // Download starten
  Job.Resume();  

  Params:=Params+' '+GUIDToString(jobId);

  Job2 := Job as IBackgroundCopyJob2;
  Job2.SetNotifyCmdLine(pWideChar(ExeName), PWideChar(Params));
  Job.SetNotifyFlags(BG_NOTIFY_JOB_TRANSFERRED);
end;

class procedure TDownloadBits.DownloadForground(ziel, downloadurl: widestring; DownloadFeedback:TDownloadProgressEvent);
var
  bi: IBackgroundCopyManager;
  job: IBackgroundCopyJob;
  jobId: TGUID;
  r: HRESULT;

  // Status Zeug
  p: BG_JOB_PROGRESS;
  s: BG_JOB_STATE;

  // Timer Zeug
  hTimer: THandle;
  DueTime: TLargeInteger;
  c: boolean;
begin
  bi:=CreateComObject(CLSID_BackgroundCopyManager) as IBackgroundCopyManager;
  r:=bi.CreateJob('Updatedownload', BG_JOB_TYPE_DOWNLOAD, JobId, job);
  if not Succeeded(r) then
    raise Exception.Create('Create Job Failed');
  r:=Job.AddFile(PWideChar(downloadurl), PWideChar(ziel));
  if not Succeeded(r) then
    raise Exception.Create('Add File Failed');
  // Download starten
  Job.Resume();

  DueTime:=-10000000;
  hTimer:=CreateWaitableTimer(nil, false, 'EinTimer');
  SetWaitableTimer(hTimer, DueTime, 1000, nil, nil, false);
  while True do
  begin
    Job.GetState(s);

    if s in [BG_JOB_STATE_TRANSFERRING, BG_JOB_STATE_TRANSFERRED] then
    begin
      Job.GetProgress(p);
      DownloadFeedback(nil, p.BytesTransferred, p.BytesTotal, dsDownloadingData, '', c);
      if c then
        break;
    end;

    if s in [BG_JOB_STATE_TRANSFERRED,
      BG_JOB_STATE_ERROR,
      BG_JOB_STATE_TRANSIENT_ERROR] then
        break;

    WaitForSingleObject(hTimer, INFINITE);
  end;
  CancelWaitableTimer(hTimer);
  CloseHandle(hTimer);
  if s=BG_JOB_STATE_TRANSFERRED then
    job.Complete();

  job:=nil;
  bi:=nil;
end;

end.

Upvotes: 3

Related Questions