TMSFNCEdgeWebBrowser -> DownloadStateChanged

The SimpleGetSyncAsStream loads file data in memory. You might want to take a look at using ResultFile directly on a TTMSFNCCloudBase instance.

LBase.Request.Clear;  
LBase.Request.Host := Edit1.Text;  
LBase.Request.Method := rmGET;  
LBase.Request.ResultFile := FileName;
LBase.Request.ResultType := rrtFile;  
LBase.ExecuteRequest(  
  procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)  
  begin  
    if ARequestResult.Success then  
      Memo1.Text := ARequestResult.ResultString  
    else  
      Memo1.Text := 'Request failed';  
  end  
  );  

Hi Pieter,

finally I found a solution ( with your help ;-)

    DL_Ready := False;
    var LBase := TTMSFNCCloudBase.Create;

    try
      LBase.Request.Clear;
      LBase.Request.Host := Edit1.Text;
      LBase.Request.Method := rmGET;
      LBase.Request.ResultFile := FileName;
      LBase.Request.ResultType := rrtFile;

      LBase.ExecuteRequest(
      procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
      begin
        if ARequestResult.Success then
          DownloadList.Items.Add(ARequestResult.ResultString)
        else
          DownloadList.Items.Add('Request failed');
        DL_Ready := True;
      end,
      procedure (const ARequestResult: TTMSFNCCloudBaseRequestResult; AProgress: Single)
      begin
        AdvProgressBar2.Position := trunc(AProgress);
      end
      );

      while not DL_Ready do
      begin
        Sleep(100);
        if Exit_all then exit;
        Application.ProcessMessages;
      end;

    finally
      LBase.Free;
    end;

Sweet, thanks for the feedback! You might want to rethink the download process, because right now, you are downloading one file at the time. TTMSFNCCloudBase can execute multiple requests at the same time, so you can create once instance of TTMSFNCCloudBase during the lifetime of your application and start multiple asynchronous requests.

Yep, I Know.

I use the { while not DL_Ready Loop } to prevent this yet.
Otherwise I run in <LBase.Free> while the download is still running.

Is it possible to have the <LBase.Free> inside the ARequestResult Function ?

Looks like a kind of LBase.Suicide :slight_smile:

Next Step is an
Array of TTMSFNCCloudBase to keep the number of downloads under control ...

EDIT:
Now I have understand what you mean :-) no Array necessary ...

EDIT:
Array of TTMSFNCCloudBaseRequestResult to keep the different request

but how can I differentiate the single request when I use a common callback function ?

TTMSFNCCloudBaseRequestResult.ResultFileName ?

OKAY, got it ..
Thank you for the advise :-)

By the way,

there are so much useful function and helper classes from TMS
but I didn't figure out yet where I can find the documentation ???

The main problem is not How to use it, but how to find it ...

Each Request info is copied to the result.

procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
end,

ARequestResult contains unique information about the request. If you want to add additionally information you can use LBase.Request.Data* properties (such as DataString, DataBoolean)

You can indeed create/destroy LBase each time a request happens, you should do that in the callback of ExecuteRequest, but best practice is to create a unique instance in your application and use that one during the lifetime of your application.

From time to time we write blogs about specific functionality.

A full quickly written sample. (I didn't test it yet, but shows how to setup one download function for multiple downloads).

unit Unit58;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  FMX.TMSFNCCloudBase,
  FMX.Controls.Presentation, FMX.StdCtrls, Generics.Collections, FMX.Layouts,
  FMX.ListBox;

type
  TDownload = class
  private
    FFileName: string;
    FURL: string;
  public
    property FileName: string read FFileName write FFileName;
    property URL: string read FURL write FURL;
  end;

  TDownloads = class(TObjectList<TDownload>);

  TForm58 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    ProgressBar1: TProgressBar;
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    LBase: TTMSFNCCloudBase;
    FDownloads: TDownloads;
    procedure DownloadFile(AURL: string);
    procedure RefreshList;
  public
    { Public declarations }
  end;

var
  Form58: TForm58;

implementation

{$R *.fmx}

procedure TForm58.DownloadFile(AURL: string);
var
  d: TDownload;
begin
  d := TDownload.Create;
  d.FFileName := 'C:\MyFiles\' + ExtractFileName(AURL);
  d.FURL := AURL;

  LBase.Request.Clear;
  LBase.Request.Host := d.FURL;
  LBase.Request.Method := rmGET;
  LBase.Request.ResultFile := d.FFileName;
  LBase.Request.ResultType := rrtFile;
  LBase.Request.DataObject := d;

  LBase.ExecuteRequest(
    procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
    var
      dl: TDownload;
    begin
      dl := TDownload(ARequestResult.DataObject);
      if Assigned(dl) then
      begin
        if ARequestResult.Success then
          FDownloads.Add(dl)
        else
          FreeAndNil(dl);
      end;

      RefreshList;
    end,
    procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult;
      AProgress: Single)
    begin
      ProgressBar1.Value := Trunc(AProgress);
    end);
end;

procedure TForm58.Button1Click(Sender: TObject);
begin
  DownloadFile('https://www.myserver.com/file1.txt');
end;

procedure TForm58.Button2Click(Sender: TObject);
begin
  DownloadFile('https://www.myserver.com/file2.txt');
end;

procedure TForm58.FormCreate(Sender: TObject);
begin
  LBase := TTMSFNCCloudBase.Create;
  FDownloads := TDownloads.Create;
end;

procedure TForm58.FormDestroy(Sender: TObject);
begin
  LBase.Free;
  FDownloads.Free;
end;

procedure TForm58.RefreshList;
var
  I: Integer;
begin
  ListBox1.Clear;
  for I := 0 to FDownloads.Count - 1 do
    ListBox1.Items.Add(FDownloads[I].FileName);
end;

end.

Thanks Pieter,

I've understood the concept.
With this in mind it will be easy to modify it for my needs.

In your example every download manipulates the same Progressbar :slight_smile:
and the download List holds only the finished Downloads,
but I have all informations I need to complete my code. Thanks again

Yes,

I'm aware there are shortcomings in my code :smiley: either way, should there be questions, let me know.

Hi Pieter,

now I run aground with another ( more basic ) problem.

I try to develop my "Download Manager" as a visual Component :sunny:

with a property Webbrowser : TMyWebBrowser;

  TMyWebBrowser = class(TTMSFNCCustomWebBrowser)
  public
    *property OnDownloadStateChanged: TTMSFNCWebBrowserOnDownloadStateChanged read FOnDownloadStateChanged write FOnDownloadStateChanged;*   
    procedure DoDownloadStateChanged(ADownload: TTMSFNCWebBrowserDownload; AState: TTMSFNCWebBrowserDownloadState; var APause: Boolean; var AResume: Boolean; var ACancel: Boolean); virtual;
  end;

The Idea is :
drop it on a Form with a TTMSFNCWebbroser and connect the property;

So far so good, but I can't figure out how to recocnice the
OnDownloadStateChanged Event from the Webbrowser,
without changing the code of Form1.TMSFNCWebBrowser1OnDownload...

So to speak : No Modification on Form Side. Just Drop Component & Connect.

The Event procedure DoDownloadStateChanged is virtual but :

...
protected
  property OnDownloadStateChanged: TTMSFNCWebBrowserOnDownloadStateChanged read FOnDownloadStateChanged write FOnDownloadStateChanged;

is protected ( even in TCustomTMSFNCWebbroser )
So I can't use the Adapter Property (Webbrowser) to assign a new event handler ( inside DL Manager Class )

Or just in one sentence : How can I publish a protected OnEvent property in child class :slight_smile:

AND : why the F.. has the DoDownloadStateChanged no ( Sender : TObject ... :grin: :zipper_mouth_face: :smile:

Just republish the event without getters & setters

TMyWebBrowser = class(TTMSFNCCustomWebBrowser)
published
  property OnDownloadStateChanged;
end;

I just updated my TMS Components with TMS Setup and the Bug disappeared :thinking:
Now all parallel downloads fires her OnBytesReceivedChanged Events until completed.

Next issue,

when downloads are interrupted
I start a timer and resume after 5 secs -> this results in ghost tasks
Sometime with wrong Webbrowser-> resultfilepath ( from parallel tasks )

After the downloads are finished, these ghost tasks still resists.
When I try to delete these ghost tasks ( downloads[i].delete ) -> Access violation

Regards Wolfgang

It could be possible that the Edge Chromium runtime update version fixed the issue, but that could be verified against the release notes. We noticed something regarding download folder being unique for each instance of WebView so that could potentially be related.

The download tasks are not removed, they are kept in memory. Can you provide sample code that demonstrates the issue?

procedure TF_Main.Timer1Timer(Sender: TObject);
begin
  for var i := 0 to WB.Downloads.Count-1  do
  begin
    if wb.Downloads[i].State = dsInterrupted then
    begin
      if (WB.Downloads[i].CanResume) then
      begin
        wb.Downloads[i].Resume; // this causes the Ghost Tasks
        WriteLogEvent('Restart',TPath.GetFileNameWithoutExtension(wb.Downloads[i].ResultFilePath), WB.Downloads[i].URI);
      end;
    end;
  end;
  Timer1.Enabled := False;
end;
procedure TF_Main.TMSFNCEdgeWebBrowser1DownloadStateChanged(Sender: TObject;
    ADownload: TTMSFNCWebBrowserDownload; AState:
    TTMSFNCWebBrowserDownloadState; var APause, AResume, ACancel: Boolean);
var
  st: string;
  ID : string;
  idx : Integer;
  Index : string;
  Studio : string;
  FileName : string;
  FilePath : string;
  Directory : string;

  ATask : ITask;

  function MoveFile(FileName : string):TProc;
  var
    FilePath : string;
  begin
    if not FileExists(FileName) then
    begin
     FilePath := TPath.GetDirectoryName(FileName);

     ForceDirectories(FilePath);
     TFile.Move(ADownload.ResultFilePath, FileName);
    end;
  end;
begin

  idx := ADownload.Index + 1;
  ID := TPath.GetFileNameWithoutExtension(ADownload.ResultFilePath);

  case AState of
    dsInProgress: st := 'Progress';

    dsInterrupted: begin
        APause := True; // set Pause to Resume after Timer1 fired
        st := 'Interrupted';

        // if not User cancel !! => without this you can force ghost task by caceling downloads
        if ADownload.InterruptReason in [dirUserCanceled, dirDownloadProcessCrashed] then
        begin
          ACancel := True; // User Cancel must be set explicite !!
          exit;
        end
        else
        begin
          Timer1.Enabled := True; // try to resume after 5 Sec.
        end;

    end;

    dsCompleted: begin
        st := 'Completed';

        FileName := ADownload.ResultFilePath;
        FileName := TPath.GetFileName(FileName);
        FileName := StringReplace(FileName,'.','\',[]); // convert first Name.Part into Dir

        FilePath := TPath.GetDirectoryName(Site.Media.Clips + FileName);

        FileName := TPath.GetFileNameWithoutExtension(FileName);

        FileName := FilePath + '\' + FileName + '.mp4';

        ATask := TTask.Create(MoveFile(FileName)).Start; // move 25GB File to NAS async

Thank you for the code snippet, we"ll investigate this as soon as possible.

Another short remark,
the OnDownloadBytesReceivedChanged
is fired with 0 Bytes in ADownload.BytesReceived or
with the same Value as transmitted in the last OnDownload.. Event

I Use the difference between the Received Bytes to calculate the DL Speed

// My Workaround for the moment
if ADownload.BytesReceived = 0 then exit;
if BytesReceivedLast = ADownload.BytesReceived then exit;

with the naming (BytesReceivedChanged) in mind I suppose this is unwanted ...

Hi,

We've investigated the code and could not reproduce the ghost tasks you are referring to, if I download a single file, there is one download it stays one download even though I pause and resume. If the user cancels, then the download is interrupted and cannot be resumed again, this is normal, as the download needs to restart. The only case when a download stays the same download is if the users pauses and resumes.