And storage for all, Smart solutions to HTML5 files
In short, it goes like this:
- You have to ask to store data into your quota
- You have to then ask to get access to the filesystem
- You have to ask for access to a file-object
- You must request a filereader or writer
- An operation may not execute straight away
Each of these steps happen inside the event handler of the previous.The tricky part here is that these are all nested, which means you can use each element only within *that* event handler. So if you try to access the filesystem object outside the event-handler where you ask for a quota change you will actually get an error; or the JS engine starts to behave oddly. I had to re-boot Chrome because it really crashed when I tried to trick it into behaving linear like we are used to under Delphi and C#.
In Smart it would look something like this:
RequestQuota(Procedure () begin requestFileSystem( procedure () begin requestFileObject( procedure () begin requestWriter( procedure () begin writedata( procedure () begin showmessage("Data written! Yeeej!"); end); end); end); end); end);
In other words — it’s a complete mess. It’s actually twice as bad as above, because you also have to define error handlers besides success handlers. Wish we could get R2/D2’s soon so we dont have to deal with code like this.
To make a long story short, the best way to deal with files is through actions. Thats right, the same glorious action system that you find in Delphi and FreePascal. In essence an action is just an object with two common methods: validate() and execute(). The validate function should return the readiness of the object (are all properties set? Can we perform the action now?), while the execute() method implements the actual activity.
I hated this implementation when I first made it, but it was the only system which would secure that your data goes from A to B safely. Initially I wanted a unified storage API where drivers was created after an analysis of the browser environment. So if you were running under phonegap, you would get a driver with full access to the filesystem. If you were running in a normal browser, then you would get a driver which saved data to local storage.
That was a good idea, but sadly the recursive callback loops could not be overcome to provide a linear, blocking storage API like we are used to under Delphi (or any native language for that matter).
The good news
The good news in all of this is that Actions perform very well! I mean, you tend to isolate reading and writing of data in one place, so creating an action object is no different from creating a file-object. And it’s the same with loading. Under HTML5 loading data on a mobile device typically involve a dialog with a spinner or something while you load your data — so triggering this from an action is easier than through 2-3 event handlers.
So how do we store files? Like this:
var mAction: TFileActionSave; begin mAction:=TFileActionSave.Create; mAction.FileName:="document.txt"; mAction.FileMode:=fmPermanent; mAction.FileData:=FStreamToSave; mAction.Execute(NIL);
Pretty neat huh? And if you want to be notified when the file is properly written to disk (or whatever storage medium you select), then you can just assign an event handler, like this:
var mAction: TFileActionSave; begin mAction:=TFileActionSave.Create; mAction.FileName:="document.txt"; mAction.FileMode:=fmPermanent; mAction.FileData:=FStreamToSave; mAction.OnExecute:=procedure (sender:Tobject) begin showmessage("File has been saved!"); TFileActionSave(sender).free; end; mAction.Execute(NIL);
Device separated actions
Actions for performing singular activities are not new, Delphi has had them for ages. And they are really helpful (especially for UI programming). But Delphi’s actions are a bit different from our type of actions here. Our type of actions are expected to be executed once, then disposed or left to be eaten by the garbage collector. You can recycle them naturally but it serves little purpose.
In the event handler above we dispose of the action in the OnExecute() handler, just to make sure no reference to the filesystem lingers in the system. You don’t have to do this, but it’s always best to write things properly. It costs little and can make a huge difference. Some browsers dont take well to having a filesystem object floating around in memory. Chrome goes bananas if more than one reference targets the same object for instance.
Either way, the really cool part is that you can now write your IO code once, and it will run on all supported platforms. If you execute this after phonegap compiling your app, it will store files on your device’s real filesystem. If you execute it in a normal browser (like you are using now) then it will store data in the sandboxed environment (of which you have a maximum of 5 megabytes to play with).
The final result
Well, having cleaned up this mess as best I can, here is the final result. I have made the example below a bit messy on purpose just to show you how simple actions are.
What this routine does is to first generate a small file, execute the “save” action — and when the save routine is done and the file is stored – we create a loading action and read it back again.
It will write out the string to the console. And if you dont believe this is the simple version, feel free to check out system.io.pas when we release this code later. I think you will be very thankful that you have Smart Mobile Studio to deal with all this so you can focus on the fun stuff – writing cool applications!
procedure TForm1.W3Button4Click(Sender: TObject); var mAction: TFileActionSave; begin mAction:=TFileActionSave.Create; mAction.FileName:="document.txt"; mAction.FileMode:=fmTemporary; mAction.OnExecute:=procedure (sender:Tobject) begin var mLoader:=TFileActionLoad.Create; mLoader.FileData:=TMemoryStream.Create; mLoader.FileName:='document.txt'; mLoader.FileMode:=fmTemporary; mLoader.OnExecute:=Procedure (sender:TObject) begin try var mReader:=TReader.Create(mLoader.FileData); writeln(mReader.ReadString); except on e: exception do writeln(e.message); end; end; mLoader.Execute; end; mAction.FileData:=TMemoryStream.Create; var mTemp:=TWriter.Create(mAction.FileData); mTemp.WriteString("This is some data"); mAction.Execute; end;
Well – enjoy!