domingo, 14 de agosto de 2016

Smart Mobile Studio - Variant

Variant data type

The Variant data type provides a flexible general purpose data type. It can hold basically anything. You can use variants when interacting with native javascript objects or external libraries.

We can also use variants in external classes. The External classes are the glue between Smart Pascal and the javascript world. It's important to say these external classes are case sensitive, in fact, the external code is only interfaced – this is not a real class, but an external class. This means that we only need the definition while the implementation is done by the external JavaScript code.

The Variants are treated as "unknown" by the smart pascal codegen. Variants are primarily used to transport native values and objects between methods dealing with javascript objects or browser objects. So Smart Mobile Studio compiler deals with variants as unknown values. Use it with care - there are potentials for run time errors and poor code clarity when using variants. For instance, the compiler does not check if a method of an object actually exists.

this

In this example, I'm going to define an external global variable. Note that, external variables must be declared in global scope. We're going to use this in global context. The this keyword behaves differently in JavaScript compared to other language. In Object Oriented languages, like smart pascal, the this keyword refers to the current instance of the class (aka Self).

In JavaScript the value of this is determined mostly by the invocation context of function and where it is called. Se, we can interface with native javascript objects with object pascal and create powerful components.

We can declare this variable using two ways:

{ using external variable }
var this external "this" : variant;
 { or using external function }
function this: variant; external 'this' property;

then we can use this external variable, such as:

procedure HandleCallback(a: variant);
begin
 console.log(a);
end;

procedure InvokeByVariant;
var mSrc: variant;
begin
mSrc := this.document;

(* invoke our method *)
HandleCallback(mSrc);
end;

Variant parameter

Let's suppose we have a method which looks like:

procedure HandleCallback(data: variant);
begin
  //
end;

In the sample code, it remains unclear the argument data. I'd call them a better name because that gives you more of a chance of understanding at a glance what your code means. You can customize it, so the method looks like this now:

type
 TJSONData = Variant;

procedure HandleCallback(data: TJSONData);
begin
//
end;

Literal object using the keyword Class

I'd like to create a JS literal object in smart pascal, like this:

var object1 = {
  "child" : {
    "Callback" : function () {
       console.log(this);
       return this;
    }
  }
};

As has been said: variants are treated as "unknown" by the smart pascal codegen. The smart compiler does no check if a method f.i. actually exists! Use it with care.






var object1 := CLASS
  that: variant = class external;

  child = class
    Callback := function(): variant
    begin
     console.log(this);
     Result := this;
    end;
  end;
END;

procedure CallMe(unknown: variant);
begin
  unknown.Name := "MyName";
  unknown.Age  := 102;
  unknown.Callback(unknown);
end;

CallMe(Object1.child);

CallMe(CLASS
  Callback := lambda
    console.log('this is a test');
  end;
END);

JS Emitted:

function CallMe(unknown) {
 unknown.Name = "MyName";
 unknown.Age = 102;
 unknown.Callback(unknown);
};

var object1 = {
 "child" : {
 "Callback" : function () {
    var Result = undefined;
    console.log(this);
    Result = this;
    return Result
 }
 }
 ,"that" : {
 }
};
CallMe(object1.child);
CallMe({
 "Callback" : function () {
 console.log("this is a test");
 }
});   

Array of Array of Integer

Now I'd like an object such as: {"values":[[0,100],[1000,200],[2000,300],[3000,400],[4000,500]] }

I've got weird result using this approach:

var preloaded = CLASS
  values : array [0..4] of array [0..1] of Integer = [ [0, 100],[1000, 200],[2000, 300],[3000, 400],[4000, 500] ];
END;

console.log(JSON.stringify(preloaded));
{"values":[[4000,500],[1000,200],[2000,300],[3000,400],[4000,500]]}

an easy workaround would be defining a local variable like this:

var valor : array [0..4] of array [0..1] of Integer = [ [0, 100],[1000, 200],[2000, 300],[3000, 400],[4000, 500] ];

var preloaded2 = CLASS
  values := valor;
END;

console.log(JSON.stringify(preloaded2));
{"values":[[0,100],[1000,200],[2000,300],[3000,400],[4000,500]]}

JSON Array Object

Now I would like to create a JSON object like this:

[{"id":1,"name":"Item#1"},{"id":2,"name":"Item#2"},{"id":3,"name":"Item#3"},{"id":4,"name":"Item#4"},{"id":5,"name":"Item#5"},{"id":6,"name":"Item#6"},{"id":7,"name":"Item#7"},{"id":8,"name":"Item#8"},{"id":9,"name":"Item#9"},{"id":10,"name":"Item#10"}]


var obj := CLASS
  that: variant = class external;

  child = class
    Callback := function(): variant
    begin
     Result := this;
    end;
  end;

  "JSONArray1": array of variant;

  "JSONArray2": array of variant;

  "JSONArray3": array of variant;

  "JSArray": array of variant;

  "name": string = "obj";

  "f"   : TFunc = function(): variant
          begin
            result := this.that;
          end;
END;



for var x:=1 to 10 do
begin
  obj.JSONArray1.add(CLASS
    name := "Item#" + IntToStr(x);
    id := x;
  END);
end;

Javascript Array

...and a simple javascript array list like:
["Item#1", "Item#2", "Item#3", "Item#4", "Item#5", "Item#6", "Item#7", "Item#8", "Item#9", "Item#10"]


for var x:=1 to 10 do
begin
  obj.JSArray.add("Item#" + IntToStr(x));
end;



Array of JSON Array

Playing more, with smart pascal, how about to create this weird array of array of objects?

[[{"age":1,"nome":"Nome#1"},{"cep":"35700-001","address":"Addr#1"},{"country":"Country 1","city":"City#1"}],[{"age":2,"nome":"Nome#2"},{"cep":"35700-002","address":"Addr#2"},{"country":"Country 2","city":"City#2"}],[{"age":3,"nome":"Nome#3"},{"cep":"35700-003","address":"Addr#3"},{"country":"Country 3","city":"City#3"}],[{"age":4,"nome":"Nome#4"},{"cep":"35700-004","address":"Addr#4"},{"country":"Country 4","city":"City#4"}],[{"age":5,"nome":"Nome#5"},{"cep":"35700-005","address":"Addr#5"},{"country":"Country 5","city":"City#5"}],[{"age":6,"nome":"Nome#6"},{"cep":"35700-006","address":"Addr#6"},{"country":"Country 6","city":"City#6"}],[{"age":7,"nome":"Nome#7"},{"cep":"35700-007","address":"Addr#7"},{"country":"Country 7","city":"City#7"}],[{"age":8,"nome":"Nome#8"},{"cep":"35700-008","address":"Addr#8"},{"country":"Country 8","city":"City#8"}],[{"age":9,"nome":"Nome#9"},{"cep":"35700-009","address":"Addr#9"},{"country":"Country 9","city":"City#9"}],[{"age":10,"nome":"Nome#10"},{"cep":"35700-0010","address":"Addr#10"},{"country":"Country 10","city":"City#10"}]]

In my experiments, 
for var x:=1 to 10 do
begin
  var objeto :=
    [CLASS
      nome   : string  = "Nome#" + IntToStr(x);
      age    : integer =  + x;
    END,
    CLASS
      address: string  = "Addr#" + IntToStr(x);
      cep    : variant = '35700-00'+ IntToStr(x);
    END,
    CLASS
      city   : string  = "City#" + IntToStr(x);
      country: string  = "Country " + IntToStr(x);
    END];
obj.jsonarray2.push(objeto);
end;

 The codegen generated weird pusha javascript instruction. I had to replace to push to get working.

var obj = null,
 x$32 = 0;
var objeto = [null,null,null];
/// anonymous TClassSymbol
/// anonymous TClassSymbol
TApplication.InitApp(Self);
obj = {
 "that" : {
 }
 ,"name" : "obj"
 ,"JSONArray3" : []
 ,"JSONArray2" : []
 ,"JSONArray1" : []
 ,"JSArray" : []
 ,"f" : function () {
 var Result = undefined;
 Result = this.that;
 return Result
 }
 ,"child" : {
 "Callback" : function () {
    var Result = undefined;
    Result = this;
    return Result
 }
 }
};
for(x$32=1;x$32<=10;x$32++) {
 /// anonymous TClassSymbol
 /// anonymous TClassSymbol
 /// anonymous TClassSymbol
 objeto = [{
 "age" : x$32
 ,"nome" : "Nome#"+x$32.toString()
 }, {
 "cep" : "35700-00"+x$32.toString()
 ,"address" : "Addr#"+x$32.toString()
 }, {
 "country" : "Country "+x$32.toString()
 ,"city" : "City#"+x$32.toString()
 }];
 obj.JSONArray2.pusha(objeto.slice(0));
}

to be continued...

It would be handy in SMS, in this instance, to have a kind of TDataModule - collection of data stores. How to use in-memory data stores in Smart. This TDataModule should have agnostic functions like findById, findByName, etc..
  
I ended up spending a decent amount of time trying different things to get this TW3DataSet  as in memory dataStore. To save some headaches, I decided to create my own data store, it’s ugly, unfinished, I find it more flexible to use it through the units. I don't know if the right stack, anyway, stay tuned.

segunda-feira, 8 de agosto de 2016

Smart Mobile Studio - Hybrid UI

I can say that is possible to write complex applications with Smart Mobile Studio, you just have to know its limitations. Limitations, you will also face with other tools like TypeScript. For instance, depending on what you plan to do, you can use only the SMS IDE, and use pure Object Pascal, of course, you have to do everything by hand, at least you have control over everything.

Sooner or later, you will stumble working on the user interface. This is often the biggest challenge facing any mobile development project. SMS offer a solution to the user interface - the SCL framework.

SCL approach

As you know, the SMS UI part is handled by the SCL (SmartCL Framework). It has a basic component library, including source code. Simply dragging and dropping visual components on the visual designer, and changing their appearance and behaviors as desired with the Object Inspector, like the old Delphi 7.

You even can create new components based upon existing components and add then to the component library. These components responds to events, typically user actions, and it invokes changes on the view.

Hand-written code

One thing that I tend to disagree is using the RAD approach to build, develop and manage the user interface. I sincerely do not like of this methodology, which I believe is difficult to work on real projects (with a lot of views).

I'll prefer hand-written code (write HTML/CSS code manually), I consider to be more readable, more maintainable and believe to offer more customization and abstractions. CSS is all about presentation, and it takes a good amount of tweaking to get things looking right across different browsers.
Using this approach, the visual designer is not required.

Hybrid Approach

SCL (SmartCL framework) has its strengths, removing completely SmartCL* components from your smart project can severely restrict the use of the some nice features.

The short of it is: You’re not going to be able to build an entire application in SMS Visual Designer (at least not yet anyway), but it is an nice tool for quickly creating UI and prototype apps.

What is a hybrid? The proposal is using hybrid designer-developer combo.  Part developer (views created using the visual designer / SCL), part designer (views polished by true designers).

Once you understand the limitations of the SmartCL*, what is and what isn't possible currently in your design. Some views, can be created by using the visual designer. Another part —  a talented person, a HTML5/CSS designer. I’m convinced that hand-coding is an essential to create beautiful views. Hand-coding also lets you create smaller files than a software package. It's faster to create finished, tidy web page templates by hand-coding than it is to use a WYSIWYG editor, for instance.

Using this methodology, it is possible to interfacing some mostly used frameworks like ionics, F7, Material Design UI with the SCL visual designer. Using this idea will give us plenty of benefits.  

In the following experiment, I'm going to use the hybrid methodology. The intro, blog, gallery, tabs, login, social, video and contact views should be created by using hand coding approach. Only two views (Form1 and Form2) were created using the SMS visual designer.

My 2 cents

I, myself, know how to code pascal to some extent. But I find myself coding less and less, ultimately. Since designing a more complex component task is time consuming. I still feel obliged to learn and pick up new coding skills every once in a while, which is nice – I always got overloaded styling a component.

I'm frustrating with the UI processes, instead of focusing on the smart strengths - on the logic, the smart compiler to write and debug business logic in its powerful language. Using the current approach we need to be a rockstart designer-developer to design beautiful designers.

quinta-feira, 4 de agosto de 2016

Working with small JSON data sets in memory using Smart Mobile Studio

TW3DataSet

TW3Dataset is simply a small in-memory datastore that allowed you to save small data. You can save data to a normal string or stream. It also can be used as an intermediate format, you can both push data to a server (small number of records) as well as retrieve data from a server. 
If you want to persist some JSON information in the browser. Depending on user interaction with the application, you want to store 5-6 different JSON object into memory, the TW3DataSet is an option. This is great to create prototyping applications.
Note: storing large data in memory has a couple of disadvantages:
  • non-scalable — when you decide to use more processes, each process will need to make same api request;
  • fragile — if your process crashes you will lose the data.
Also working with large amount of data can block process for longer time than you would like.Solution: I'll use external storage! It can be MongoDB or RDBMS; - update data in separate process, triggered with cron; - don't drop the whole database: there is a chance that someone will make a request right after that (if your storage doesn't support transactions, of course), update records. Working with large JSON datasets can be a pain, particularly when they are too large to fit into memory.  

Example JSON

The following is an example of the JSON for the Products table. The JSON returned by the Delphi web server have this format: 

[{
 "productid" : "9V-BATTERY-12PK",
 "description" : "12-pack of 9-volt batteries",
 "listprice" : 20,
 "shipping" : 2
 }, {
 "productid" : "9V-BATTERY-4PK",
 "description" : "4-pack of 9-volt batteries",
 "listprice" : 4.5,
 "shipping" : 1.5
 }
]

We can fill a combobox using TW3Dataset, like in this example:

Define/Create the dataset 

Take a closer look at the above JSON format returned by our server (column and row data).  Before we create a dataset, we have to define what the table looks like, we have to define the field-definition property.
Ensure to add the System.Dataset unit;
  ProductsDS := TDataset.Create;
  
  ProductsDS.FieldDefs.Add('productid',ftString);
  ProductsDS.fieldDefs.Add('description',ftString);
  ProductsDS.fieldDefs.Add('listprice',ftFloat);
  ProductsDS.fieldDefs.add('shipping',ftFloat);
  ProductsDS.CreateDataset;
 DataSet Columns: When we define manually a dataset and the SaveToString method is called from the application.
The following JSON format is returned by our application:
{
 "dhMagic" : 51966,
 "dhCount" : 0,
 "dhFieldDefs" : {
  "ddMagic" : 3401235116,
  "ddDefs" : [{
    "fdName" : "productid",
    "fdDatatype" : 4
   }, {
    "fdName" : "description",
    "fdDatatype" : 4
   }, {
    "fdName" : "listprice",
    "fdDatatype" : 3
   }, {
    "fdName" : "shipping",
    "fdDatatype" : 3
   }
  ]
 },
 "dhData" : []
}
Field Types: Observe the following details the various column types and how they should be specified:
Field Type Code Description
ftUnknown 0 Unknown type - not specified collumn type
ftBoolean 1 Boolean
ftInteger 2 Integer
ftFloat 3 Float
ftString 4 String
ftDateTime 5 DateTime/Float
ftAutoInc 6 generated field
ftGUID 7 generated field

Adding records

This task is more or less identical to how you would do it under Delphi.
You have both append and insert operations. Let's use Append method for this example:

  procedure fillProductsDS;
  begin
    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := '9V-BATTERY-12PK';
    ProductsDS.Fields.FieldByName('description').AsString := '12-pack of 9-volt batteries';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 20;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := '9V-BATTERY-4PK';
    ProductsDS.Fields.FieldByName('description').AsString := '4-pack of 9-volt batteries';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 4.5;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 1.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'CALCULATOR-BUSINESS';
    ProductsDS.Fields.FieldByName('description').AsString := 'Business calculator';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 10;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 1;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'CASH-REGISTER';
    ProductsDS.Fields.FieldByName('description').AsString := 'Cash register with thermal printer';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 170;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 10;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'FLASH-USB-16GB';
    ProductsDS.Fields.FieldByName('description').AsString := '16GB USB flash drive';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 15;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'FLASH-USB-32GB';
    ProductsDS.Fields.FieldByName('description').AsString := '32GB USB flash drive';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 25;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'FLASH-USB-8GB';
    ProductsDS.Fields.FieldByName('description').AsString := '8GB USB flash drive';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 10;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'LABEL-MAKER';
    ProductsDS.Fields.FieldByName('description').AsString := 'Label maker - plastic labels';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 35;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PEN-BP-12PK';
    ProductsDS.Fields.FieldByName('description').AsString := '12-pack of ballpoint pens';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 12;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.6;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PHONE-HEADSET';
    ProductsDS.Fields.FieldByName('description').AsString := 'Hands-free phone headset';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 15;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PHONE-SYSTEM-4HS';
    ProductsDS.Fields.FieldByName('description').AsString := '4-handset phone system with main base';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 120;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 4;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PROJECTOR-HD';
    ProductsDS.Fields.FieldByName('description').AsString := '1080p HD Projector';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 850;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 56;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'SCANNER-SF';
    ProductsDS.Fields.FieldByName('description').AsString := 'Sheet-feed paper scanner';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 150;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 7;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'SHREDDER-SF-CC';
    ProductsDS.Fields.FieldByName('description').AsString := 'Sheet-feed, cross-cut shredder with bin';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 8;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 10;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'USB-CARD-READER';
    ProductsDS.Fields.FieldByName('description').AsString := 'USB magnetic strip card reader';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 25;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;
  end;

  { fill Customer dataset }  
  fillProductsDS;

 

Dataset row JSON structure

DataSet Rows: Observe the following details on the column "dhData", when the SaveToString method was called. The field "dhData" was populated, of course this could be requested from the web server using HTTP GET request.

{
 "dhMagic" : 51966,
 "dhCount" : 15,
 "dhFieldDefs" : {
  "ddMagic" : 3401235116,
  "ddDefs" : [{
    "fdName" : "productid",
    "fdDatatype" : 4
   }, {
    "fdName" : "description",
    "fdDatatype" : 4
   }, {
    "fdName" : "listprice",
    "fdDatatype" : 3
   }, {
    "fdName" : "shipping",
    "fdDatatype" : 3
   }
  ]
 },
 "dhData" : [{
   "productid" : "9V-BATTERY-12PK",
   "description" : "12-pack of 9-volt batteries",
   "listprice" : 20,
   "shipping" : 2
  }, {
   "productid" : "9V-BATTERY-4PK",
   "description" : "4-pack of 9-volt batteries",
   "listprice" : 4.5,
   "shipping" : 1.5
  }, {
   "productid" : "CALCULATOR-BUSINESS",
   "description" : "Business calculator",
   "listprice" : 10,
   "shipping" : 1
  }, {
   "productid" : "CASH-REGISTER",
   "description" : "Cash register with thermal printer",
   "listprice" : 170,
   "shipping" : 10
  }, {
   "productid" : "FLASH-USB-16GB",
   "description" : "16GB USB flash drive",
   "listprice" : 15,
   "shipping" : 0.5
  }, {
   "productid" : "FLASH-USB-32GB",
   "description" : "32GB USB flash drive",
   "listprice" : 25,
   "shipping" : 0.5
  }, {
   "productid" : "FLASH-USB-8GB",
   "description" : "8GB USB flash drive",
   "listprice" : 10,
   "shipping" : 0.5
  }, {
   "productid" : "LABEL-MAKER",
   "description" : "Label maker - plastic labels",
   "listprice" : 35,
   "shipping" : 2
  }, {
   "productid" : "PEN-BP-12PK",
   "description" : "12-pack of ballpoint pens",
   "listprice" : 12,
   "shipping" : 0.6
  }, {
   "productid" : "PHONE-HEADSET",
   "description" : "Hands-free phone headset",
   "listprice" : 15,
   "shipping" : 2
  }, {
   "productid" : "PHONE-SYSTEM-4HS",
   "description" : "4-handset phone system with main base",
   "listprice" : 120,
   "shipping" : 4
  }, {
   "productid" : "PROJECTOR-HD",
   "description" : "1080p HD Projector",
   "listprice" : 850,
   "shipping" : 56
  }, {
   "productid" : "SCANNER-SF",
   "description" : "Sheet-feed paper scanner",
   "listprice" : 150,
   "shipping" : 7
  }, {
   "productid" : "SHREDDER-SF-CC",
   "description" : "Sheet-feed, cross-cut shredder with bin",
   "listprice" : 8,
   "shipping" : 10
  }, {
   "productid" : "USB-CARD-READER",
   "description" : "USB magnetic strip card reader",
   "listprice" : 25,
   "shipping" : 2
  }
 ]
}

 

Loading/Saving records

TW3Dataset allows you to save your data to a normal string or a stream.

function    SaveToString:String;
Procedure   LoadFromString(Const aText:String);
 
Procedure   SaveToStream(const Stream:TStream);virtual;
Procedure   LoadFromStream(const Stream:TStream);virtual;
So we can store a dataset locally with SaveToString/SaveToStream methods
and retrieve the data using LoadFromString/LoadFromStream methods.
Example:

ProductsDS := TDataset.Create;
ProductsDS.LoadFromString( jsonData );
ProductsDS.CreateDataset;
This will load locally a dataset. We can use it as intermediate bridge, we can insert, append, delete data locally, and push the data to a server. In this example, let's just list two Products fields:

ProductsDS.Active := true;

procedure ListProductsDS;
begin
ProductsDS.first;
while not ProductsDS.EOF do
begin
  var id  := ProductsDS.fields.fieldbyname('productid').asString;
  var price := ProductsDS.fields.fieldbyname('listprice').asString;
  writeln(id + ' ' + price);
  ProductsDS.Next;
end;
end;

{ List ProductsDS }
ListProductsDS;

/*
9V-BATTERY-12PK   20
9V-BATTERY-4PK   4.5
CALCULATOR-BUSINESS     10
CASH-REGISTER   170
FLASH-USB-16GB   15
FLASH-USB-32GB   25
FLASH-USB-8GB   10
LABEL-MAKER   35
PEN-BP-12PK   12
PHONE-HEADSET   15
PHONE-SYSTEM-4HS  120
PROJECTOR-HD   850
SCANNER-SF   150
SHREDDER-SF-CC   8
USB-CARD-READER  25
*/

 

Load remote data

Another nice feature is that you can define your dataset locally and load data remotely. Let's suppose our Delphi REST server is returning this:
{"rows" : [{
 "productid" : "9V-BATTERY-12PK",
 "description" : "12-pack of 9-volt batteries",
 "listprice" : 20,
 "shipping" : 2
}, {
 "productid" : "9V-BATTERY-4PK",
 "description" : "4-pack of 9-volt batteries",
 "listprice" : 4.5,
 "shipping" : 1.5
}, {
 "productid" : "CALCULATOR-BUSINESS",
 "description" : "Business calculator",
 "listprice" : 10,
 "shipping" : 1
}, {
 "productid" : "CASH-REGISTER",
 "description" : "Cash register with thermal printer",
 "listprice" : 170,
 "shipping" : 10
}, {
 "productid" : "FLASH-USB-16GB",
 "description" : "16GB USB flash drive",
 "listprice" : 15,
 "shipping" : 0.5
}, {
 "productid" : "FLASH-USB-32GB",
 "description" : "32GB USB flash drive",
 "listprice" : 25,
 "shipping" : 0.5
}, {
 "productid" : "FLASH-USB-8GB",
 "description" : "8GB USB flash drive",
 "listprice" : 10,
 "shipping" : 0.5
}, {
 "productid" : "LABEL-MAKER",
 "description" : "Label maker - plastic labels",
 "listprice" : 35,
 "shipping" : 2
}, {
 "productid" : "PEN-BP-12PK",
 "description" : "12-pack of ballpoint pens",
 "listprice" : 12,
 "shipping" : 0.6
}, {
 "productid" : "PHONE-HEADSET",
 "description" : "Hands-free phone headset",
 "listprice" : 15,
 "shipping" : 2
}, {
 "productid" : "PHONE-SYSTEM-4HS",
 "description" : "4-handset phone system with main base",
 "listprice" : 120,
 "shipping" : 4
}, {
 "productid" : "PROJECTOR-HD",
 "description" : "1080p HD Projector",
 "listprice" : 850,
 "shipping" : 56
}, {
 "productid" : "SCANNER-SF",
 "description" : "Sheet-feed paper scanner",
 "listprice" : 150,
 "shipping" : 7
}, {
 "productid" : "SHREDDER-SF-CC",
 "description" : "Sheet-feed, cross-cut shredder with bin",
 "listprice" : 8,
 "shipping" : 10
}, {
 "productid" : "USB-CARD-READER",
 "description" : "USB magnetic strip card reader",
 "listprice" : 25,
 "shipping" : 2
}
]}
The dataset rows must be loaded at run-time, and the data rows can come from the web server application in JSON format. When the rows are loaded, you can specify that the rows be appended to the existing rows in the dataset, or completely replace the current rows in the dataset.
The above example of the JSON for the Products dataset returned by a server.
We can fill the predefined dataset remotely, from a JSON string, for instance:
procedure fillProductsDS(strJSON: string);
begin
  var resultSet := JSON.Parse(strJSON).rows;
  for i in resultSet do
  begin
    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := resultSet[i].productid;
    ProductsDS.Fields.FieldByName('description').AsString := resultSet[i].description;
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := resultSet[i].listprice;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := resultSet[i].shipping;
    ProductsDS.Post;
  end;
end;

fillProductsDS(dataJSON);

Tags: TW3Dataset; in-memory datastore; JSON store.

domingo, 31 de julho de 2016

Generic Report with Smart Mobile Studio

How to create dynamically generic reports in Smart Pascal. Something like:

var                                    
   Report: BaseReport;                  
 begin                                  
   Report:= CreateGenericReport( <T> ); 
 end;

 

Does SmartMS supports generics?
Smart Pascal does not support generics. The original syntax derives from Delphi Web Script, was compatible with Delphi 7. Although the DWScript codebase and language has evolved considerably over the years, generics, represents a monumental change that might require a complete re-write of the entire parser and AST ("abstract symbolic tree") in compiler.

I'd try creating a single factory class in pure smart way, that does not uses generics to allow you to change the key (the key, in this case is a string representing the report name) used to request an report instance.  

You simply pass the key value and the static method that will be invoked to do the creation. Lastly, I want to retrieve a particular instance of my report by passing the key value, for instance.

Remember however, the type of the key value and the base type of the returned objects should be configurable.

LIVE PREVIEW

Let's say you have a reporting application. You may have a base report and a bunch of descendant reports, and you want to enforce some consistency over property values for the different reports that are set at the time of creation.

This is the base class TBaseReport:

unit uBaseReport;

interface

type
  TBaseReport = class(TObject)
  published
    function BuildReport: string; virtual;
end;

implementation

uses uReportA, uReportB, uReportC;

function TBaseReport.BuildReport: string;
begin
  Result := 'master report';
end;

end.

and three descendant classes:

unit uReportA;

interface

uses 
uBaseReport;

type
  TReportA = class(TBaseReport)
  published
    function BuildReport: string; override;
end;

implementation

function TReportA.BuildReport: string;
begin
  // TODO
end;

end.

 

unit uReporB;

interface

uses 
  uBaseReport;

type
  TReportB = class(TBaseReport)
  published
    function BuildReport: string; override;
end;

implementation

function TReportB.BuildReport: string;
begin
  // TODO
end;

end.

 

unit uReporC;

interface

uses 
  uBaseReport;

type
  TReportC = class(TBaseReport)
  published
    function BuildReport: string; override;
end;

implementation

function TReportC.BuildReport: string;
begin
  // TODO
end;

end.

 

The idea is try to centralise the creation logic for classes.

Having these spread around every different place a report could be created may be error-prone.

A Report Factory can give you one place to make sure all reports get created in the certain way, and also let the client code simply request a report by name and have the correct concrete report class created for them.

So, let's re-implement the uBaseReport unit

 

nit uBaseReport;

interface

uses W3C.Console;

type
TBaseReport = class(TObject)
  published
    function BuildReport: string; virtual;
  end;

  TModelClass = class of TBaseReport;

  TModelFactory = class
  public
    class function CreateModelFromID(const AID: string): TBaseReport;
    class function FindModelClassForId(const AID: string): TModelClass;
    class function GetModelClassID(AModelClass: TModelClass): string;
    class procedure RegisterModelClass(const AID: string;
      AModelClass: TModelClass);
  end;

implementation

uses uReportA, uReportB, uReportC;

{ TModelFactory }

type
  TModelClassRegistration = record
    ID: string;
    ModelClass: TModelClass;
  end;

var
  RegisteredModelClasses: array of TModelClassRegistration;

class function TModelFactory.CreateModelFromID(const AID: string): TBaseReport;
var
  ModelClass: TModelClass;
begin
  ModelClass :=  FindModelClassForId(AID);
  if ModelClass <> nil then
    Result := ModelClass.Create
  else
    Result := nil;
end;

class function TModelFactory.FindModelClassForId(
  const AID: string): TModelClass;
var
  i, Len: integer;
begin
  Result := nil;
  Len := Length(RegisteredModelClasses);
  for i := 0 to Len - 1 do
    if RegisteredModelClasses[i].ID = AID then begin
      Result := RegisteredModelClasses[i].ModelClass;
      break;
    end;
end;

class function TModelFactory.GetModelClassID(AModelClass: TModelClass): string;
var
  i, Len: integer;
begin
  Result := '';
  Len := Length(RegisteredModelClasses);
  for i := 0 to Len - 1 do
    if RegisteredModelClasses[i].ModelClass = AModelClass then begin
      Result := RegisteredModelClasses[i].ID;
      break;
    end;
end;

class procedure TModelFactory.RegisterModelClass(const AID: string;
  AModelClass: TModelClass);
var
  i, Len: integer;
begin
  Assert(AModelClass <> nil);
  Len := Length(RegisteredModelClasses);
  for i := 0 to Len - 1 do
    if (RegisteredModelClasses[i].ID = AID)
      and (RegisteredModelClasses[i].ModelClass = AModelClass)
    then begin
      Assert(FALSE);
      exit;
    end;
  RegisteredModelClasses.SetLength(Len+1);
  
  RegisteredModelClasses[Len].ID := AID;
  RegisteredModelClasses[Len].ModelClass := AModelClass;
end;

function TBaseReport.BuildReport: string;
begin
  Result := 'master report';
end;

end.

Remember however, the type of the key value and the base type of the returned objects should be configurable.

In the initialization session let's fill the configurable data:

initialization
  var rec :TModelClassRegistration;
  rec.ID := 'devil_report';
  rec.ModelClass := TReportA;
  RegisteredModelClasses.Add(rec);
  
  rec.ID:= 'flowers_report';
  rec.ModelClass := TReportB;
  RegisteredModelClasses.Add(rec);
  
  rec.ID := 'cards_report';
  rec.ModelClass := TReportC;
  RegisteredModelClasses.Add(rec);

With that, we've got a very reusable Factory class,

Now, create a form, add the uBaseReport, SmartCL.Controls.Combobox, SmartCL.Controls.Memo units, and create config the ComboBox and Memo components:

 

unit Form1;

interface

uses 
  uBaseReport,
  SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.  Forms, 
  SmartCL.Fonts, SmartCL.Borders, SmartCL.Application,
  SmartCL.Controls.Combobox, SmartCL.Controls.Memo;

type
  TForm1 = class(TW3Form)
    procedure ComboBox1Changed(Sender: TObject);
  private
    {$I 'Form1:intf'}
    div1: TW3Memo;
    ComboBox1: TW3ComboBox;
  protected
    procedure InitializeForm; override;
    procedure InitializeObject; override;
    procedure Resize; override;
  end;

implementation

{ TForm1 }

procedure TForm1.ComboBox1Changed(Sender: TObject);
begin
  if ComboBox1.SelectedIndex >= 1 then
  begin
    var rpt:= TModelFactory.CreateModelFromID( ComboBox1.Items[ComboBox1.SelectedIndex] );
    div1.InnerHTML := rpt.BuildReport;
  end;
end;

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components
end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}
  div1 := TW3Memo.Create(Self);
  div1.Width := 264;
  div1.Top := 128;
  div1.StyleClass := 'memo';
  div1.Left := 104;
  div1.Height := 208;
  div1.Name := 'div1';

  ComboBox1 := TW3ComboBox.Create(Self);
  ComboBox1.Width := 264;
  ComboBox1.Top := 88;
  ComboBox1.Left := 104;
  ComboBox1.Height := 32;
  ComboBox1.Name := 'ComboBox1';
  ComboBox1.OnChanged := ComboBox1Changed;

  ComboBox1.BeginUpdate;
  ComboBox1.Clear;
  ComboBox1.Add('Select a report');
  ComboBox1.Add('devil_report');
  ComboBox1.Add('flowers_report');
  ComboBox1.Add('cards_report');
  ComboBox1.EndUpdate;
end;
 
procedure TForm1.Resize;
begin
  inherited;
end;
 
initialization
  Forms.RegisterForm({$I %FILE%}, TForm1);
end.

I should have no excuse for avoiding them in the future. Anyway, hopefully this little experiment has sparked some thoughts in you. 

 

Another idea, would be create stores, it would be nice you have for instance a generic data stores, you can switch from Delphi REST mORMot data store to in-memory data store, but this is another story.

just curious to see the javascript output:

var TObject = {
 $ClassName : "TObject",
 $Parent : null,
 ClassName : function (s) {
  return s.$ClassName
 },
 ClassType : function (s) {
  return s
 },
 ClassParent : function (s) {
  return s.$Parent
 },
 $Init : function () {},
 Create : function (s) {
  return s
 },
 Destroy : function (s) {
  for (var prop in s)
   if (s.hasOwnProperty(prop))
    delete s.prop
 },
 Destroy$ : function (s) {
  return s.ClassType.Destroy(s)
 },
 Free : function (s) {
  if (s !== null)
   s.ClassType.Destroy(s)
 }
}
var Exception = {
 $ClassName : "Exception",
 $Parent : TObject,
 $Init : function () {
  FMessage = ""
 },
 Create : function (s, Msg) {
  s.FMessage = Msg;
  return s
 }
}
function $W(e) {
 return e.ClassType ? e : Exception.Create($New(Exception), e.constructor.name + ", " + e.message)
}
function $NewDyn(c, z) {
 if (c == null)
  throw Exception.Create($New(Exception), "ClassType is nil" + z);
 var i = {
  ClassType : c
 };
 c.$Init(i);
 return i
}
function $New(c) {
 var i = {
  ClassType : c
 };
 c.$Init(i);
 return i
}
function $Is(o, c) {
 if (o === null)
  return false;
 return $Inh(o.ClassType, c);
};
function $Inh(s, c) {
 if (s === null)
  return false;
 while ((s) && (s !== c))
  s = s.$Parent;
 return (s) ? true : false;
}
var TApplication = {
 $ClassName : "TApplication",
 $Parent : TObject,
 $Init : function ($) {
  TObject.$Init($);
 },
 RunApp : function (Self) {
  var m = null,
  p = "",
  n = null;
  m = TModelFactory.FindModelClassForId(TModelFactory, "devil_report");
  console.log(TObject.ClassName(m));
  p = TModelFactory.GetModelClassID(TModelFactory, m);
  console.log(p);
  n = TModelFactory.CreateModelFromID(TModelFactory, "flowers_report");
  console.log(n);
  TBaseReport.showmsg(n, "this is the smart report");
 },
 Destroy : TObject.Destroy
};
var TModelFactory = {
 $ClassName : "TModelFactory",
 $Parent : TObject,
 $Init : function ($) {
  TObject.$Init($);
 },
 CreateModelFromID : function (Self, AID) {
  var Result = null;
  var ModelClass$1 = null;
  ModelClass$1 = TModelFactory.FindModelClassForId(Self, AID);
  if (ModelClass$1) {
   Result = TObject.Create($NewDyn(ModelClass$1, ""));
  } else {
   Result = null;
  }
  return Result
 },
 FindModelClassForId : function (Self, AID$1) {
  var Result = null;
  var i = 0;
  var Len = 0;
  Result = null;
  Len = RegisteredModelClasses.length;
  var $temp1;
  for (i = 0, $temp1 = Len; i < $temp1; i++) {
   if (RegisteredModelClasses[i].ID$2 == AID$1) {
    Result = RegisteredModelClasses[i].ModelClass;
    break;
   }
  }
  return Result
 },
 GetModelClassID : function (Self, AModelClass) {
  var Result = "";
  var i$1 = 0;
  var Len$1 = 0;
  Result = "";
  Len$1 = RegisteredModelClasses.length;
  var $temp2;
  for (i$1 = 0, $temp2 = Len$1; i$1 < $temp2; i$1++) {
   if (RegisteredModelClasses[i$1].ModelClass == AModelClass) {
    Result = RegisteredModelClasses[i$1].ID$2;
    break;
   }
  }
  return Result
 },
 Destroy : TObject.Destroy
};
var TBaseReport = {
 $ClassName : "TBaseReport",
 $Parent : TObject,
 $Init : function ($) {
  TObject.$Init($);
 },
 showmsg : function (Self, str) {
  console.log(str);
 },
 Destroy : TObject.Destroy
};
function Copy$TModelClassRegistration(s, d) {
 d.ID$2 = s.ID$2;
 d.ModelClass = s.ModelClass;
 return d;
}
function Clone$TModelClassRegistration($) {
 return {
  ID$2 : $.ID$2,
  ModelClass : $.ModelClass
 }
}
var TReportA = {
 $ClassName : "TReportA",
 $Parent : TBaseReport,
 $Init : function ($) {
  TBaseReport.$Init($);
 },
 Destroy : TObject.Destroy
};
var TReportB = {
 $ClassName : "TReportB",
 $Parent : TBaseReport,
 $Init : function ($) {
  TBaseReport.$Init($);
 },
 Destroy : TObject.Destroy
};
var TReportC = {
 $ClassName : "TReportC",
 $Parent : TBaseReport,
 $Init : function ($) {
  TBaseReport.$Init($);
 },
 Destroy : TObject.Destroy
};
var Application = null;
var RegisteredModelClasses = [], rec = {
 ID$2 : "",
 ModelClass : null
};
rec.ID$2 = "devil_report";
rec.ModelClass = TReportA;
RegisteredModelClasses.push(Clone$TModelClassRegistration(rec)); ;
rec.ID$2 = "cards_report";
rec.ModelClass = TReportB;
RegisteredModelClasses.push(Clone$TModelClassRegistration(rec)); ;
rec.ID$2 = "flowers_report";
rec.ModelClass = TReportC;
RegisteredModelClasses.push(Clone$TModelClassRegistration(rec)); ;

var m = null, p = "", n = null;
m = TModelFactory.FindModelClassForId(TModelFactory, "devil_report");
console.log(TObject.ClassName(m));

p = TModelFactory.GetModelClassID(TModelFactory, m);
console.log(p);

n = TModelFactory.CreateModelFromID(TModelFactory, "flowers_report");
console.log(n);

TBaseReport.showmsg(n, "this is the smart report");