Monday, March 12, 2012

Show "loading animation" during long Intraweb AJAX requests

Yesterday I was talking to another Intraweb developer and I've created a demo application to show him how I lock the screen, showing the loading AJAX animation (IWLocker), during an AJAX request that takes long time to return. After that, I've improved my demo using 3 different techniques do do that. In this post I will show you all the 3 techniques.

The problem

Suppose you have a IWButton in a form and want to trigger an async (AJAX) request that will take a long time to return. For instance, you will create a complex report, or generate a complex file to be downloaded later. You want your user to click the button and have some kind of feedback that your application is working, yes? At the same time you don't want your user to keep clicking all over the place... The best option, IMO, is tho show the IWLocker (the IW AJAX "loading animation") during the request. I will show you three different techniques to do that. Very similar but you may prefer one over the other.

First solution

For this solution, I will create a JavaScript function that will be called by your button directly, without any OnAsyncClick event handler. First, you have to create the JavaScript function. Let's say:
function myAjaxFunc() { 
  var myData = "Nicole Scherzinger,Scarlett Johansson,Jennifer Lawrence,Doutzen Kroes,Charlize Theron";
  ShowBusy(true);
  executeAjaxEvent("&data="+myData, null,"IWFORM1.DoMyAjaxFunc", false, null, false);
  return true;
}
This is a simple js function: There is a variable myData that holds the data that I want to send to the server, in this case, a comma separated list of names, but it can be whatever you want. Then I call ShowBusy, and after that I call the real AJAX request, executeAjaxEvent.
Note that to show the IWLocker, I'm calling ShowBusy function. ShowBusy is declared in IWPreScript.js, a JavaScript library that is available to all your IWForms. ShowBusy expects a single boolean parameter. If you pass true the IWLocker will be shown, and if you pass false the IWLocker will be hidden. Also note that executeAjaxEvent will call IWFORM1.DoMyAjaxFunc method of your form. This method must be registered so IW core can call it. To register that method, first you have to create the method DoMyAjaxFunc in your IWForm1:
const
  CDATATag = '< ! [CDATA[%s] ] >';  // spaces added so blogger won't mess the CDATA string

procedure TIWForm1.DoMyAjaxFunc(EventParams: TStringList);
var
  ResponseFunc: string;
  sl: TStrings;
  s: string;
begin
  sl := TStringList.Create;
  try
    sl.StrictDelimiter := True;
    sl.CommaText := EventParams.Values['data'];
    s := sl.Strings[Random(sl.Count)];
  finally
    sl.Free;
  end;
  Sleep(5000);  // simulate a long operation
  IWLabel1.Caption := 'The hot chick chosen is: ' + s;
  ResponseFunc := Format(CDATATag, ['ShowBusy(false);']);
  WebApplication.CallBackResponse.AddJavaScriptToExecute(ResponseFunc);
end;
The code above shows the DoMyAjaxFunc. It retrieves the "data" parameter sent from myAjaxFunc() and simulates a long operation (in this case, 5 seconds). When this method finishes, it sends ShowBusy(false) to the browser using WebApplication.CallBackResponse.AddJavaScriptToExecute method.
Then we must register DoMyAjaxFunc callback, using:
WebApplication.RegisterCallBack(UpperCase(Self.Name) + '.DoMyAjaxFunc', DoMyAjaxFunc);  // Self.Name -> IWForm1
Finally you must call the JavaScript function myAjaxFunc from some JavaScript event directly, let's say in the onClick button event handler:
procedure TIWForm1.IWAppFormCreate(Sender: TObject);
begin
  IWButton1.ScriptEvents.HookEvent('onClick', 'myAjaxFunc();');
end;
I hooked myAjaxFunc() to the onClick event handler of IWButton1 in runtime. It can be done in design time, using Object Inspector as well.
That's it! If you run your application and click on the IWButton1, you will see that IWLocker will become visible for 5 seconds (the time that DoMyAjaxFunc() method takes to run) and then will be hidden again.

Second solution

The second solution is very similar to the first, but instead of calling your myAjaxFunc() directly from your onClick event handler, you can use IW async events to call it, in this case, IWButton2 (another button in IWForm1) onAsyncClick event:
const
  CDATATag = '< ! [CDATA[%s] ] >';  // spaces added so blogger won't mess the CDATA string

procedure TIWForm1.IWButton2AsyncClick(Sender: TObject; EventParams: TStringList);
var
  ResponseFunc: string;
begin
  ResponseFunc := Format(CDATATag, ['ShowBusy(true);myAjaxFunc();']);
  WebApplication.CallBackResponse.AddJavaScriptToExecute(ResponseFunc);
end;
Inside IWButton2 OnAsyncClick event handler I add JavaScript to execute, again using AddJavaScriptToExecute method. The JavaScript code that will be executed is a call to ShowBusy (showing the IWLocker) and then a call to myAjaxFunc();.

Third solution

The third solution is a little different from the others. It uses the IWButton3 OnAsyncClick twice. Let's see how:
procedure TIWForm1.IWButton3AsyncClick(Sender: TObject;
  EventParams: TStringList);
var
  ResponseFunc: string;
  FirstCall: boolean;
begin
  FirstCall := (EventParams.Values['SecondCall'] <> 'true');
  if FirstCall then  // this code will run in the first IWButton3AsyncClick call
  begin
    ResponseFunc := Format(CDATATag, ['ShowBusy(true);executeAjaxEvent("&SecondCall=true", null,"' + IWButton3.HTMLName + '.DoOnAsyncClick", false, null, false);']);
  end else   
  begin   // this code will run in the second IWButton3AsyncClick call
    Sleep(5000);  // simulate a long operation
    ResponseFunc := Format(CDATATag, ['ShowBusy(false);']);
    IWLabel1.Caption := 'Result returned';
  end;
  WebApplication.CallBackResponse.AddJavaScriptToExecute(ResponseFunc);
end;
When you click the IWButton3, the OnAsyncClick event is fired. You then search for some parameter, in this case "SecondCall" in the EventParams list. If SecondCall is not there, then this is the first OnAsyncClick call. In the first call we add some JavaScript code to be executed. This code shows the IWLocker (calling ShowBusy(true)), and then calls the OnAsyncClick event again, this time passing the SecondCall parameter to the method.
When the event OnAsyncClick is fired the second time, there will be a SecondCall parameter in EventParams list and then we know that this time we must do the "real" work. When the method finishes, it will then call ShowBusy(false); hiding the IWLocker again.

Conclusions

The three techniques have the same effect: Show IWLocker during a long AJAX async request. Using the first method, your application will use a single AJAX request to your server, but you have to write some JavaScript code. Using the third method you won't have to write a single line of JavaScript but there are two AJAX requests involved. If you are not comfortable writing JavaScript you may prefer this solution. The second has the worst of both worlds: Two AJAX requests involved and some JavaScript coding... ;-)

Download the sample project

You can download a complete project showing all the three methods here.

Enjoy!

2 comments:

CastleSoft said...

Did you release the "Doing nice things with Intraweb + jQuery, JQGrid" source code ? Couldn't find it anywhere on your site.

Anonymous said...

Hello, I am not able to retrieve the file via download link. Could you share your code via attachment news group in the embarcadero forums.