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");