¡Dar una buena imagen! Parte 2

Si os parece bien, recapitulamos: Comentaba al finalizar la entrada anterior, ¡Dar una buena imagen! Parte 1,   que estábamos en condiciones de proseguir, toda vez que habíamos generado una base de datos y almacenado algunas imágenes en ella, que era uno de los requisitos que podía demandar  el hecho de habernos planteado «devolver una imagen a través de nuestro servidor de DataSnap». No es que realmente nos hiciera falta explícitamente contar con una base de datos, pero recordando lo compartido en aquellas lineas, me parecía un poco mas didáctico a efectos del blog y de las reflexiones que motivara su contenido. Por otro lado, hacer el cambio, y sustituir este origen por el de un fichero gráfico, era sencillo y bastaba cambiar un par de lineas. Así pues, y resumiendo, tomamos la decisión de guardar la imagen en la base de datos en un campo cuyo tipo de dato es adecuado a su contenido, habitualmente binario, en lugar de conservar la ruta donde éste está ubicado, y creamos una pequeña utilidad para alimentar y gestionar la tabla de imágenes.

Menciones a métodos en DataSnap

Sería bueno que nos detuviéramos por un momento, antes de seguir adelante, y dedicáramos unas lineas a hablar, o reflexionar, sobre los métodos en DataSnap, que no siempre existieron tal y como ahora los entendemos, y que fueron una muy buena aportación tras la llegada de Embarcadero y aplicar cambios importantes y profundos en esta tecnología.

Los métodos remotos fueron uno de esos cambios importantes. En la docwiki de Embarcadero  existe bastante información, -aunque nunca sea suficiente-, y de hecho, he buscado, para que pudieran quedar reflejados en esta segunda parte, algunos enlaces que nos puedan ayudar, de forma que nos permitan introducir el tema. Concretamente, el enlace principal que lo abre, Developing DataSnap Applicatións, que es uno de esos enlaces que es bueno tener a mano ya que, además de los apuntes hacia las distintas referencias al tema de la docwiki, se enumeran una buena cantidad de recursos externos, enlazados desde vídeos, código fuente y artículos técnicos. También tengo anotados por su utilidad, el enlace que abre el tema propiamente de los métodos Exposing DataSnap Server Methods, y el que hace referencia y enumera los distintos tipos de datos que se soportan dentro del marco de DBExpress Data.DBXCommon.TDBXDataTypes.

Sin embargo, si nos referimos en concreto a la información sobre el uso de parámetros de tipo TStream, Jim Tierney dejó escritos varios artículos imprescindibles, en los que se aborda el tema con bastante detalle. Uno de estos enlaces es DataSnap Server Method Stream Parameters, cuya lectura es necesaria, así como revisar el código fuente que enlaza desde http://cc.embarcadero.com/item/26854.

En la parte tercera de esta serie de artículos, al final de la misma, intentaré recopilar todos los enlaces que me sea posible encontrar y que os puedan ser necesarios e interesantes. De hecho, durante los dos años anteriores y este, se incrementa la documentación técnica con la aportación de algunos artículos de compañeros de nuestra comunidad, que han ido introduciendo en la red las distintas áreas de conocimiento de datasnap, aportando su granito de arena. Por supuesto, gran parte de esa posible lista serán recursos de habla inglesa.

Consideraciones previas a tener en cuenta

Todos tenemos una idea intuitiva de qué es un método remoto expuesto por el servidor de DataSnap, porque a fin de cuentas, no dejan de ser nada distinto de los métodos que habitualmente expone una clase. DataSnap, nos permite así, desde una aplicación cliente, invocar métodos que se ejecutan en otra aplicación que actúa como un servicio y que atiende o queda a la escucha de estas peticiones, bien a través del protocolo http o https, bien a través de tcp/ip. Básicamente es eso.

A menudo y de forma corporativa, hemos conocido esta imagen que resume y ofrece una vista amplia de DataSnap.

Permitid me que deje algunas ideas en el aire, aun a costa de simplificar.

Dejado aparte todas aquellas clases que colaboran en la parte digamos propia de la estructura del servicio (las que crean propiamente el servicio como tal, fijan y configuran los distintos canales, abriendo o cerrando comunicaciones, etc.), quizás la primera consideración que se me ocurre para ayudaros en esa primera toma de contacto, es resaltar que solo van a poder exponer métodos remotos las clases descendientes TDataModule y TDSServerModule, así como cualquier descendiente de TPersistent, pero esta ultima opción requeriría de vuestra parte un esfuerzo adicional ya que os obligaría a implementar las capacidades de servicio que ya vienen introducidas desde TDataModule o TDSServerModule. No parece lo mas habitual, por lo que, a efectos prácticos, consideraremos que vuestra atención se va a centrar realmente en estas dos clases, que son las que usareis en vuestro día a día.

La segunda idea es que un TDSServerModule es un descendiente de la clase TDataModule y por lo tanto, extiende la clase y la funcionalidad de ésta. Básicamente  la diferencia que os guiará en la necesidad de hacer uso de una u otra será, que vuestro modulo de datos en el servidor deba exportar datasets hacia el cliente. En ese caso, para que sean visibles estos datasets y podais conectarlos a través del driver de datasnap, el contendor será la clase TDSServerModule.

Si damos un vistazo a la unidad DataSnap.DSServer, veremos su interfaz como sigue:

{$MethodInfo ON}
 TDSServerModule = class(TDSServerModuleBase)
 end;
 {$MethodInfo OFF}

Esto enlaza con algo que nos comentaba desde la lectura de la ayuda en linea, cuando se nos dice que es necesario aplicar la directiva {$MethodInfo} para que los métodos sean visibles desde la clase TDataModule. Esta directiva, {$MethodInfo on/off} trabaja conjuntamente con la que ayuda a nuestro compilador a generar información RTTI (Run Time Type Library)  de clase, {$TYPEINFO  ON/OFF}, haciendo necesario que ambas estén activas en el estado de la clase. De hecho, cuando nos dicen que los descendientes de TPersistent van a poder exponer métodos remotos es porque esta clase -TPersistent- activa la información de tipos y por lo tanto es una posible candidata en el nivel mas elevado de esa jerarquía, cuyos descendientes van a heredar también dicho estado. La directiva {$TYPEINFO} es equivalente a  {$M+}.

Podemos entender el papel de la RTTI, si consideramos que precisamente, la generación de esta metadata adicional en el servidor, ayudará al sistema a descubrir los métodos a través de una cadena de texto literal enviada en el mensaje compartido entre la aplicacion cliente y la aplicación servidor, cadena de texto que representa la clase y el método a invocar por el servidor. Si repasamos, por ejemplo, las lineas generadas automáticamente en la unidad proxy, que es una plantilla que se crea a petición del usuario con la información de los métodos que descubre el servidor en el contexto de la aplicación cliente, para facilitarnos no tener que mantener esta tarea que puede ser repetitiva y tediosa, la implementación que recrea la invocación del método EchoString se presenta como sigue:

function TServerMethods1Client.EchoString(Value: string): string;
begin
 if FEchoStringCommand = nil then
 begin
 FEchoStringCommand := FDBXConnection.CreateCommand;
 FEchoStringCommand.CommandType := TDBXCommandTypes.DSServerMethod;
 FEchoStringCommand.Text := 'TServerMethods1.EchoString';
 FEchoStringCommand.Prepare;
 end;
 FEchoStringCommand.Parameters[0].Value.SetWideString(Value);
 FEchoStringCommand.ExecuteUpdate;
 Result := FEchoStringCommand.Parameters[1].Value.GetWideString;
end;

He incluido en negrita la cadena que identifica el método a invocar. Dicha cadena representa la clase que contiene el método y su nombre. De no existir esa información adicional extra, asociada a la clase, no seriamos capaz de encontrar ni los actores ni las acciones. En agosto del 2007 compartimos una entrada en el blog que os puede ayudar a entender este punto: La Granja y en la que precisamente vemos como invocar un método en la clase a través de su nombre. Creo recordar que hacíamos trotar graciosamente a la clase TCaballo, además de otras tropelias como alimentarle o permitir que relinche. 😀  La RTTI se nos presenta a varios niveles: desde un nivel mínimo que hace que una instancia sea capaz de reconocerse en una clase, que podríamos llamar «autoconciencia» 😀 , hasta un nivel más extendido que permite a la clase describirse y reconocerse en toda la funcionalidad que el desarrollador de la misma autorice a conocer y describir. Las zonas publicadas (published) de la clase, forman parte de esas zonas que generan información extendida y metadata adicional que la RTTI ya recogía con la directiva {$M+}. Al activar la directiva  {$MethodInfo} ayudamos a que la zona publica de la clase genere también esa información extra de tipos que de otra forma no se generaría.

La tercera idea, en la que me gustaría incidir es que una gran parte de los métodos, se nos presentan íntimamente ligados a la información de tipos. Es decir, podemos encontrar la necesidad de ejecutar un procedimiento remoto que no necesite parámetros pero lo habitual es que existan parámetros de entrada y un tipo de retorno, en el caso de las funciones. Así que en este punto nos podemos hacer la pregunta que este compañero me hacia entre lineas: ¿Que tipo de dato necesito declarar como retorno de mi función, para exportar la imagen? ¿Y si necesito una fecha? ¿Puedo retornar un tipo propio de la logica de mi aplicación, como por ejemplo una instancia de la clase TCliente?

La respuesta a estas preguntas, la encontramos en los enlaces que compartíamos en las lineas superiores de la entrada. En el primero de ellos, encontramos lo que buscamos: No todos lo tipos son soportados como parámetros.

…but not all the parameter types are supported. The supported types are:

En la entrada de Jim Tierney los encontráis clasificados y agrupados de una forma mas clara, incluyendo algunos que no se mencionan explicitamente en la lista.

Basic Basic DBXValue Collection Connection
  • AnsiString
  • Boolean
  • Currency
  • TDateTime
  • TDBXDate
  • TDBXTime
  • Double
  • Int64
  • Integer
  • LongInt
  • OleVariant
  • Single
  • SmallInt
  • WideString
  • TDBXAnsiStringValue
  • TDBXAnsiCharsValue
  • TDBXBcdValue
  • TDBXBooleanValue
  • TDBXDateValue
  • TDBXDoubleValue
  • TDBXInt16Value
  • TDBXInt32Value
  • TDBXInt64Value
  • TDBXSingleValue
  • TDBXStringValue
  • TDBXTimeStampValue
  • TDBXTimeValue
  • TDBXWideCharsValue
  • TDBXWideStringValue
  • TDBXReader
  • TDataSet
  • TParams
  • TStream
Collection DBXValue
  • TDBXReaderValue
  • TDBXStreamValue
  • TDBXConnection
Connection DBXValue
  • TDBXConnectionValue

Por lo tanto, ahora sí estamos en posición de responder a las preguntas que  nos formulábamos. No existe un parámetro que identifique directamente a una instancia de TImage o de la clase TBitmap. Sin embargo, genéricamente nos podemos apoyar en el uso de una clase como TStream que permite manipular esta información como una secuencia de bytes, optando por cualquiera de los descendientes que veamos apropiado para el contenido real.

También ahora estamos en condiciones de entender porque en el ejemplo del Restaurante, Luis Alfonso Rey se puede apoyar en la clase TDataSet como retorno de una de las funciones que su servidor exportaba. Recordad que el código fuente de esta demo que sirvió en la exposición durante las ultimas presentaciones del partner español se encuentra a vuestra disposición en el link de la entrada inmediatamente anterior a esta.

Primer paso: Nuestro servidor de datasnap

Aunque sea repetitivo, he incluido las capturas de los primeros pasos, para que aquellos que se inician no tengan que tirar de imaginación o memoria.

Lo primero de todo, vamos a crear la aplicación que va a dar el servicio, nuestro servidor de datasnap. Va a ser un servidor básico, basado en tcp/ip y no expondrá sus servicios a través de http.

Accedemos al menú FILE -> NEW -> OTHER -> DataSnapServer. Seleccionamos la opción DataSnap Server entre las tres disponibles.

paso1_nuevo_servidor

Una vez en ese punto, accederemos a un wizard que nos va a permitir configurar el servidor. He seleccionado la opción VCL Forms Application ya que no va a ser un servicio integrado en el sistema ni un proyecto de tipo consola sino que será una aplicación monda y lironda.

paso2_tipo_proyecto

La siguiente parada será para determinar como va a ser el servidor. Nuestro wizard necesita conocer como va a ser ese servidor, que capa de transporte de datos va a usar, si existirán filtros, si es una comunicación segura y si además, deseamos que incluya los métodos de ejemplo que se añaden de ayuda. En el caso del servidor básico se incluyen los métodos EchoString y ReverseString, por defecto pero para servidores de tipo Rest, el sistema incluye adicionalmente un test en formato html capaz de evaluar los métodos que exporta el servidor, incluyendo los administrativos.

paso3_funcionalidades

En el siguiente paso, vamos a poder hacer un test al puerto 211, que es el que por defecto asigna DataSnap.

paso4_seleccion_puerto

Y finalmente, seleccionamos de que clase heredará la creada por nosotros para albergar los distintos métodos que exportará el servidor. Yo he elegido TDSServerModule ya que, en este caso concreto, sí voy a incluir al menos un dataset. Existirá un componente TSQLQuery que he llamado Imagenes_Interbase con el fin de que se pueda recorrer el contenido de la tabla.

paso5_seleccion_ancestro

 

Al finalizar el wizard, se habrá creado el proyecto en el que existirán varias unidades, que representan respectivamente el formulario de la aplicación, el módulo que contiene los componentes que generan y configuran la estructura del servicio y un modulo adicional que el sistema por defecto denomina ServerMethodsUnit1, conteniendo la clase que va a exponer los servicios. En dicho modulo están publicados los métodos de ejemplo que mencionábamos.

Dejadme que escriba unas lineas de código.

Dad le un vistazo al formulario, tal y como queda tras añadir los componentes y comentamos.

formulario_servidor

Nuestro TDSserverModule, es muy sencillo y contiene un query para recorrer la tabla y que sea el usuario desde la aplicación cliente quien pueda seleccionar cual de las imágenes quiere que sea devuelta por el método remoto, y un segundo query que efectivamente la devolverá mediante el método adecuado, apoyándose en el indice de esa imagen como parámetro de entrada. Para ello, parametrizamos la consulta de GetImageBanner_Intebase, que recibe en un integer la clave primaria que identifica la imagen que deseamos:

Select Banner
from Banners
where IDBanner = :IDBanner

Veamos el código que hemos escrito a tal efecto:

unit ServerMethodsUnit1;

interface

uses System.SysUtils, System.Classes, Datasnap.DSServer, Datasnap.DSAuth,
  Data.DBXMSSQL, Data.FMTBcd, Data.DB, Data.SqlExpr, Datasnap.Provider, Data.DBXJSON,
  Data.DBXInterBase, Vcl.Dialogs;

type
  TServerMethods1 = class(TDSServerModule)
    demos_interbase: TSQLConnection;
    Imagenes_Interbase: TSQLQuery;
    dspImagenes_Interbase: TDataSetProvider;
    Imagenes_InterbaseIDBANNER: TIntegerField;
    Imagenes_InterbaseBANNER: TBlobField;
    GetImageBanner_Interbase: TSQLQuery;
    dspGetImageBanner_Interbase: TDataSetProvider;
    GetImageBanner_InterbaseBANNER: TBlobField;
    procedure demos_interbaseBeforeConnect(Sender: TObject);
  private
    { Private declarations }
    function GetImagenBannerAsStream(ASQLQuery: TSQLQuery; AIDBanner: Integer): TStream; overload;
  public
    { Public declarations }
    function EchoString(Value: string): string;
    function ReverseString(Value: string): string;
    function GetImagenBannerAsJSON(AIDBanner: Integer): TJSONArray;
    function GetImagenBannerAsStream(AIDBanner: Integer): TStream; overload;
  end;

implementation

{$R *.dfm}

uses System.StrUtils, UMainServer, Data.DBXJSONCommon;

function TServerMethods1.GetImagenBannerAsStream(ASQLQuery: TSQLQuery; AIDBanner: Integer): TStream;

var
  stream: TMemoryStream;
begin
  if (ASQLQuery.Active) then ASQLQuery.Close();

  ASQLQuery.ParamByName('IDBanner').Value := AIDBanner;
  ASQLQuery.Open();

  stream := TMemoryStream.Create;

  stream.WriteData(ASQLQuery.FieldByName('Banner').AsBytes,
                   Length(ASQLQuery.FieldByName('Banner').AsBytes));
  //stream.SaveToFile('test_ahora.jpg');
  stream.Seek(0, System.Classes.TSeekOrigin.soBeginning);

  ASQLQuery.Close();

  Result:= stream;
end;

function TServerMethods1.GetImagenBannerAsJSON(AIDBanner: Integer): TJSONArray;
var
  stream: TStream;
begin
  stream:= GetImagenBannerAsStream(AIDBanner);
  try
    Result:= TDBXJSONTools.StreamToJSON(stream, 0, stream.Size);
  finally
    stream.Free;
  end;
end;

function TServerMethods1.GetImagenBannerAsStream(AIDBanner: Integer): TStream;
begin
  Result:= GetImagenBannerAsStream(GetImageBanner_Interbase, AIDBanner);
end;

procedure TServerMethods1.demos_interbaseBeforeConnect(Sender: TObject);
begin
  demos_interbase.Params.Values['Database'] := MainServer.GetPath;
end;

function TServerMethods1.EchoString(Value: string): string;
begin
  Result := Value;
end;

function TServerMethods1.ReverseString(Value: string): string;
begin
  Result := System.StrUtils.ReverseString(Value);
end;

end.

La pregunta mas interesante que yo me haría en vuestro caso, a la vista de la inspección ocular del código, podría ser:

Cuando escribimos una asignación al valor de retorno de la función, como el que podemos ver en la linea inmediatamente inferior, ¿qué sucede con la reserva de memoria que se ha hecho para gestionar el contenido del stream a devolver?

Result:= GetImagenBannerAsStream(GetImageBanner_Interbase, AIDBanner);

Fijémonos que la función privada, GetImagenBannerAsStream, declara una variable local de tipo TMemoryStream que va a ser instanciada cuando devolvemos un TStream, sin que por ningún lado se gestione que ésta sea liberada. Sin embargo, al escribir la función que retornará esa misma imagen en el interior de una estructura del tipo TJSONArray, si que puedo liberar la memoria, ya que esa memoria asociada a la instancia del stream no forma parte del retorno sino que el método de clase

TDBXJSONTools.StreamToJSON( )

hace su propia reserva para la estructura TJSONArray y copia en ella el stream. Una de las cosas que aprendí, al dar mis primeros pasos en el entorno, es que cada palo aguantaba su propia vela y que si en algún momento he gestionado la reserva de memoria por el motivo que sea, debo responsabilizarme de su liberación. Eso sucede cuando instanciamos objetos, seres «inferiores» 🙂 sin padre que les cobije. Tener un propietario, es una garantía que se responsabilizara de lo que suceda con esa criatura. Los componentes tienen esos privilegios así como sus descendientes. ¡Gente con suerte!

Sin embargo para un TStream o cualquiera de sus multiples caras, el sistema encuentra una solución pactada. El parámetro InstanceOwner, de tipo boolean, media en el dilema abierto. Os leo un párrafo de la entrada de Jim Terney.

Accessing Values

The GetStream and SetStream methods have an InstanceOwner parameter.  Passing True indicates that DBX owns the stream and will free it.  Passing False indicates that the caller owns the stream.  To control how the generated proxy classes call SetStream and GetStream, there is an AInstanceOwner parameter on the proxy class constructor:

Si a continuación observamos con detenimiento los métodos generados por el proxy para devolver el stream

function TServerMethods1Client.GetImagenBannerAsStream(AIDBanner: Integer): TStream;
begin
 if FGetImagenBannerAsStreamCommand = nil then
 begin
 FGetImagenBannerAsStreamCommand := FDBXConnection.CreateCommand;
 FGetImagenBannerAsStreamCommand.CommandType := TDBXCommandTypes.DSServerMethod;
 FGetImagenBannerAsStreamCommand.Text := 'TServerMethods1.GetImagenBannerAsStream';
 FGetImagenBannerAsStreamCommand.Prepare;
 end;
 FGetImagenBannerAsStreamCommand.Parameters[0].Value.SetInt32(AIDBanner);
 FGetImagenBannerAsStreamCommand.ExecuteUpdate;
 Result := FGetImagenBannerAsStreamCommand.Parameters[1].Value.GetStream(FInstanceOwner);
end;

La definición del parámetro FInstanceOwner en GetStream( ) es la que establece como se va a liberar la memoria reservada previamente, de forma que no existan problemas derivados de su no liberación. FInstanceOwner se establecía con un valor en el mismo constructor de la clase que va a exponer los métodos

constructor TServerMethods1Client.Create(ADBXConnection: TDBXConnection; AInstanceOwner: Boolean);
begin
 inherited Create(ADBXConnection, AInstanceOwner);
end;

En este caso, tomaba el valor True (lo veréis en el interior del evento onCreate del formulario cliente, que usará el proxy). Es lógico que sea el cliente el que decida qué hacer con esa memoria y al asignar True lo que implica es que sea el propio sistema el que se encargue de liberar esa memoria y por lo tanto, no tendremos que hacer una llamada explicita para ello. Cuando no sea necesaria esa información, en una llamada posterior, el sistema podrá decidir si libera la instancia antes de obtener un nueva invocación.

Segundo paso: Nuestra aplicación cliente.

 Para hacer la aplicación cliente, no vamos a crear mas que un formulario, aunque esto no sea demasiado ortodoxo.

Os muestro una imagen del formulario que también es muy básico y sencillo. El usario pulsará el boton Abrir DataSet y podrá elegir una de las imagenes en el componente TImage inferior. Luego, al pulsar uno de los dos botones de la parte derecha, se ejecutaría uno de los dos métodos a los que alude.

 

formulario_cliente

Este es el código que he escrito a tal efecto.

unit UMainClient;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
  System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
  Data.DBXDataSnap, IPPeerClient, Data.DBXCommon, Data.FMTBcd, Data.DB,
  Data.SqlExpr, FMX.Objects, Data.Bind.EngExt, Fmx.Bind.DBEngExt,
  System.Bindings.Outputs, Fmx.Bind.Editors, Data.Bind.Components,
  Data.Bind.DBScope, Datasnap.DBClient, Datasnap.DSConnect, FMX.Layouts,
  Fmx.Bind.Navigator, UProxy, FMX.ListBox;

type
  TForm1 = class(TForm)
    Image1: TImage;
    DataSnapCONNECTION: TSQLConnection;
    Banners: TClientDataSet;
    DSProviderConnection1: TDSProviderConnection;
    BannersBanner: TBlobField;
    BindSourceDB1: TBindSourceDB;
    BindingsList1: TBindingsList;
    LinkPropertyToField1: TLinkPropertyToField;
    BindNavigator1: TBindNavigator;
    btnDataset: TButton;
    btnMethodJSON: TButton;
    Image2: TImage;
    Label1: TLabel;
    BannersIDBanner: TIntegerField;
    LinkPropertyToField2: TLinkPropertyToField;
    cbxSelector: TComboBox;
    Interbase: TListBoxItem;
    btnMethodStream: TButton;
    Panel1: TPanel;
    labBaseDeDatos: TLabel;
    procedure btnDatasetClick(Sender: TObject);
    procedure btnMethodJSONClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure cbxSelectorChange(Sender: TObject);
    procedure btnMethodStreamClick(Sender: TObject);
  private
    { Private declarations }
    FInstanceOwner : Boolean;
    FServerMethods1Client: TServerMethods1Client;
    function GetServerMethods1Client: TServerMethods1Client;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses Data.DBXJSONCommon;

procedure TForm1.btnDatasetClick(Sender: TObject);
begin
  Banners.Open;
end;

procedure TForm1.btnMethodJSONClick(Sender: TObject);
var
   fStream: TMemoryStream;
   i: Integer;
begin
  if Banners.IsEmpty then
     raise Exception.Create('Debes abrir primero el dataset');
  try
    i:= BannersIDBanner.AsInteger;
    fStream:= TMemoryStream.Create();
    case cbxSelector.ItemIndex of
      0: begin
           fStream.LoadFromStream(TDBXJSONTools.JSONToStream(GetServerMethods1Client.GetImagenBannerAsJSON(i)));
           //fStream.SaveToFile('......test_interbase_jason_fmx.jpg');
         end;
    end;
    Image2.Bitmap.LoadFromStream(fStream);
  finally
    fStream.Free;
  end;
end;

procedure TForm1.btnMethodStreamClick(Sender: TObject);
var
   fStream: TMemoryStream;
   i: Integer;
begin
  if Banners.IsEmpty then
     raise Exception.Create('Debes abrir primero el dataset');
  try
    i:= BannersIDBanner.AsInteger;
    fStream:= TMemoryStream.Create();
    case cbxSelector.ItemIndex of
      0: begin
           fStream.LoadFromStream(GetServerMethods1Client.GetImagenBannerAsStream(i));
           //fStream.SaveToFile('......test_interbase_stream_fmx.jpg');
         end;
    end;
    Image2.Bitmap.LoadFromStream(fStream);
  finally
    fStream.Free;
  end;
end;

procedure TForm1.cbxSelectorChange(Sender: TObject);
begin
    Banners.Close;
    case cbxSelector.ItemIndex of
      0: Banners.ProviderName:= 'dspImagenes_Interbase';
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FInstanceOwner := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FServerMethods1Client.Free;
end;

function TForm1.GetServerMethods1Client: TServerMethods1Client;
begin
  if FServerMethods1Client = nil then
    FServerMethods1Client:= TServerMethods1Client.Create(DataSnapCONNECTION.DBXConnection, FInstanceOwner);
  Result := FServerMethods1Client;
end;

end.

Y aquí tenéis algunas capturas de la aplicación en ejecución, con una animación mostrando el resultado.

aplicacion_cliente
demo_parte2

En el video se puede ver la aplicación ejecutandose

 

Conclusión y comentarios adicionales

A tenor de lo visto, ambos métodos funcionan correctamente y conseguimos cargar sin problemas tanto desde un valor de retorno con un tipo TStream, como considerar un valor de tipo TJSONArray. En imágenes pequeñas ambos trabajan correctamente.

Ahora bien, mientras montaba el ejemplo para compartirlo con este compañero en un correo privado, me encontré que generaba un error si la imagen era de un tamaño mediano o grande desde

GetServerMethods1Client.GetImagenBannerAsStream(i)

donde emerge el error que mostraba la animación: «Out of memory while expanding memory stream». TJSON array funcionaba correctamente en cualquier caso.

Así que os podéis imaginar que parte de tiempo de demora de todas estas lineas ha estado en asegurarme que realmente era así e intentar descartar que no fuera un problema generado por mi propio código.

Yo creo que nosotros, como MVPs, o como personas comprometidas con la Comunidad, no podemos ocultar que exista un error puesto que no sería moral ni actuaria en conciencia. Mas al contrario,  requiere de nosotros la responsabilidad de que perdamos un poco de nuestro tiempo intentando averiguar que lo produce, si es que realmente al final es producido por alguna linea de la librería y no de nuestro desarrollo. Creo que es un buen signo de salud de la Comunidad y por ello, he añadido unas lineas para intentar capturar la naturaleza del problema y con mi pésimo inglés, intentar subirlo a QC para que se pueda solucionar. Quizás ya está documentado. Quizás sea una regresión fruto de cambios posteriores. Ni idea. Es algo que veré en los próximos días.

Podéis descargar el código fuente imagenesdatasnap y si deseáis seguir la lectura, voy a incluir algo de código para analizarlo.

¿Podemos aislar el problema?

Creo que podemos olvidarnos por un momento de que el contenido de un TStream sea gráfico o que tenga otra naturaleza. Por eso, se me ocurre invocar una función que devuelva un tamaño en relación de un parámetro de entrada, entero, que lo fije.

La función GetStreamBySize( ) devolverá un TStream de un tamaño de ASizeBytes, de tipo Integer. Su implementación es sencilla. Fijamos el tamaño de una matriz de Bytes en función ASizeBytes y sobrescribimos con un numero cualquiera cada byte. Yo he usado un 1. Y finalmente devolvemos la instancia de la clase TBytesStream que es creada en base al tamaño de la matriz de bytes.

Este es el código del servidor:

unit ServerMethodsUnit1;

interface

uses System.SysUtils, System.Classes, Datasnap.DSServer, Datasnap.DSAuth;

type
  TServerMethods1 = class(TDSServerModule)
  private
    { Private declarations }
  public
    { Public declarations }
    function EchoString(Value: string): string;
    function ReverseString(Value: string): string;
    function GetStreamBySize(ASizeBytes: Integer): TStream;  //By Bytes
  end;

implementation

{$R *.dfm}

uses System.StrUtils;

function TServerMethods1.EchoString(Value: string): string;
begin
  Result := Value;
end;

function TServerMethods1.GetStreamBySize(ASizeBytes: Integer): TStream;
var
  fStream: TBytesStream;
  fBytes: TBytes;
  i: Integer;
begin
  SetLength(fBytes, ASizeBytes);
  for i := 0 to Length(fBytes) - 1 do fBytes[i] := 1;
  fStream:= TBytesStream.Create(fBytes);
  Result:= fStream;
end;

function TServerMethods1.ReverseString(Value: string): string;
begin
  Result := System.StrUtils.ReverseString(Value);
end;

end.

El formulario cliente consta de un par de botones y una casilla de texto que nos permite probar con distintos tamaños. También  generamos un log del resultado, devuelto en las líneas del componente TMemo.

test_client

Este es el código del cliente.

unit UMainClient_fmx;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
  System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
  Data.DBXDataSnap, IPPeerClient, Data.DBXCommon, FMX.Objects, Data.DB,
  Data.SqlExpr, UProxy, FMX.Layouts, FMX.Memo, FMX.Edit;

type
  TMainClienteFMX = class(TForm)
    btnImg1: TButton;
    btnImg2: TButton;
    DataSnapCONNECTION: TSQLConnection;
    Image1: TImage;
    btnTest: TButton;
    Memo1: TMemo;
    btnCancel: TButton;
    numbox: TEdit;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnTestClick(Sender: TObject);
    procedure btnCancelClick(Sender: TObject);
  private
    { Private declarations }
    FFin: Boolean;
    FInstanceOwner : Boolean;
    FServerMethods1Client: TServerMethods1Client;
    function GetServerMethods1Client: TServerMethods1Client;
  public
    { Public declarations }
    function Fin: Boolean;
  end;

var
  MainClienteFMX: TMainClienteFMX;

implementation

{$R *.fmx}

procedure TMainClienteFMX.btnCancelClick(Sender: TObject);
begin
  FFin:= True;
end;

procedure TMainClienteFMX.btnTestClick(Sender: TObject);
var
  fBytesStream: TBytesStream;
  i,j: Integer;
  sLog: String;
begin
  FFin:= False;
  memo1.Lines.Clear;

  fBytesStream:= TBytesStream.Create;
  try
    i:= StrToInt(numbox.Text);
    while not Fin do
    begin
      fBytesStream.LoadFromStream(GetServerMethods1Client.GetStreamBySize(i));
      sLog:= 'Size: '+IntToStr(fBytesStream.Size)+ ' -> ';
      for j := 0 to Length(fBytesStream.Bytes)-1 do
        sLog:= sLog + IntToStr(fBytesStream.Bytes[j]);
      memo1.Lines.Add(sLog);
      Inc(i);
      numbox.Text:= IntToStr(i);
      Application.ProcessMessages;
    end;
  finally
    memo1.Lines.SaveToFile('Test.txt');
    fBytesStream.Free;
  end;
end;

function TMainClienteFMX.Fin: Boolean;
begin
  Result:= FFin;
end;

procedure TMainClienteFMX.FormCreate(Sender: TObject);
begin
  FInstanceOwner := True;
end;

procedure TMainClienteFMX.FormDestroy(Sender: TObject);
begin
   FServerMethods1Client.Free;
   FServerMethods1Client:= Nil;
end;

function TMainClienteFMX.GetServerMethods1Client: TServerMethods1Client;
begin
  if FServerMethods1Client = nil then
    FServerMethods1Client:= TServerMethods1Client.Create(DataSnapCONNECTION.DBXConnection, FInstanceOwner);
  Result := FServerMethods1Client;
end;

end.

Y finalmente, podéis ver una imagen de la aplicación en ejecución, así como una animación

error

test_parte2

Descargar código fuente test_stream

Conclusiones del Test

Creo entender que la función rompe o genera el error cuando el tamaño a devolver es superior o igual a 30717 bytes, aunque sinceramente no he podido averiguar la razón. Hasta ese tamaño funciona correctamente.

Haciendo algunos puntos de parada intuyo que la gestión de memoria no es capaz de alojar el buffer que contiene el stream pero no estoy seguro por lo que intentaré en los próximos días comentarlo en QC y buscar si existe o se documentación algún error similar.

Sin embargo, también quiero decir y dejar constancia que el ejemplo que nos muestra Jim Tierney aplicado sobre XE3 funciona correctamente, y precisamente testea también la devolución de los métodos remotos que retorna un valor de tipo TStream, aunque no exactamente igual. En el código que él comparte, retorna una instancia TFileStream que contiene el log de resultados del test. Su ejemplo es muy bueno y complejo y me ha llevado algo de tiempo entenderlo, y quizás por eso no he acabado de ver qué le diferencia de este que ahora yo he expuesto. Quizás sea simplemente un error mio y me alegraría que fuera así.

Espero que haya sido didáctica esta segunda parte y que os pueda servir de algo todos estos comentarios.

Hasta esa tercera parte que en teoría cerraría la serie.

Un saludo a todos.

 

 

6 respuestas a “¡Dar una buena imagen! Parte 2

Add yours

  1. Salvador muy didáctica tu explicación, tienes vena de maestro …!

    Es un ejemplo muy práctico e interesante, hay algo que no entiendo y es el por qué la redefinición del método GetImagenBannerAsStream tanto en la sección private como public ?
    Son esas cosas de la OOP que no he podido masticar bien … 😉

    Me gusta

  2. Hola Gustavo,
    me alegra que te haya gustado. +1

    Respecto al metodo, queda en la parte privada porque no quería que el servidor lo expusiera a la clase cliente. El compilador es capaz de distinguir por los parámetros que recibe cuando debe invocar a uno u otro. Pero ciertamente, si hubieras cambiado la firma del método privado no hubiera cambiado el resultado.Así queda mas misterioso 😀 jajaja

    Inicialmente, existía en el ejemplo 2 bases de datos (sqlserver e internase) y con el selector podías cambiar de una base a otra ejecutando el método. Luego, a medida que avanzaba en la escritura de la entrada, fui cambiando el código para simplificarlo y que no quedara muy lioso, limpiándolo, pero algunos detalles quedaron y ya no los rectifique. Un ejemplo de eso es el combobox, que no tiene demasiado sentido pero realmente tampoco afecta, ni para mal ni para bien.

    Un saludo,

    Salvador

    Me gusta

  3. Hola

    Excelente ejemplo y muy ilustrativo sobre todo con to esto nuevo del FireMonkey y del LiveBinding pero tengo una pregunta ¿se podrá con el el firemonkey o con metropolis crear una aplicación con MDI? o ¿hay alguna alternativa parecida en estas nuevas tecnologías?.

    Saludos

    Me gusta

  4. Hola Hugo:

    Muchas gracias por el comentario.

    Yo le daria la vuelta al razonamiento. Observa y analiza las aplicaciones corrientes que se ejecutan en metrópolis y pregúntate si eso encaja en un esquema mdi. Posiblemente de ese análisis se desprenda que no tiene mucho sentido por lo que la respuesta es obvia. Es por decirlo de alguna forma ese dicho que dice «allá donde fueres haz lo que vieres…».
    Delphi trae asistentes para convertir los formularios al estilo metropolis y si no recuerdo mal distintos componentes que te ayudan a configurar el aspecto habitual de estas aplicaciones.

    Busca la información en la docwiky de embarcadero.
    http://docwiki.embarcadero.com/RADStudio/XE5/en/Developing_Metropolis_UI_Applications

    Un saludo,

    Salvador

    Me gusta

  5. Saludos, Excelente articulo muy ilustrativo y guiado. No tendrás por si acaso algo para que sea el servidor quien reciba el JSONArry.

    Me gusta

Deja un comentario

Blog de WordPress.com.

Subir ↑

Marina Casado

Escritora y Doctora en Literatura Española. Periodista cultural. Madrid, España

Sigo aqui

Mi rincon del cuadrilatero, ahi donde al creer que me he rendido, aun sigo peleando.

Recetas y consejos nutricionales

Indicadas para personas con diabetes, recomendadas para todos.

¡Buen camino!

ANÉCDOTAS Y REFLEXIONES SOBRE UN VIAJE A SANTIAGO…

https://lfgonzalez.visiblogs.com/

Algunas reflexiones y comentarios sobre Delphi

It's All About Code!

A blog about Delphi, C++ Builder and related technologies...

The Podcast at Delphi.org

The Podcast about the Delphi programming language, tools, news and community.

Blog de Carlos G

Algunas reflexiones y comentarios sobre Delphi

The Road to Delphi

Delphi - Free Pascal - Oxygene

La web de Seoane

Algunas reflexiones y comentarios sobre Delphi

El blog de cadetill

Cosas de programación....... y de la vida

Delphi-losophy

A Lover of Delphic Wisdom

Delphi en Movimiento

Algunas reflexiones y comentarios sobre Delphi

marcocantu.blog

Algunas reflexiones y comentarios sobre Delphi

Press F9

Algunas reflexiones y comentarios sobre Delphi

El blog de jachguate

Un blog sobre tecnología y la vida en general