Delphi AdoConnection Reconnect - delphi

I have just decided to solve the "Connection" problem when a MSSQL Database server is restarted, and the connection is dropped eternally.
The only solution so far has been to restart the program, not always so easy on server far-far away (and the problem must first be detected).
**The code below seems to be working fine, but can a skilled ADO person look deeper into the code and see any errors/problems or improvements needed with this code? **
Type
TComponentHelper = class helper for TComponent
  Procedure Reconnect(var AdoConn:TAdoConnection; ConnStr:String);
  end;
procedure TComponentHelper.Reconnect(var AdoConn: TAdoConnection; ConnStr: String);
begin
  if Assigned(AdoConn) then begin
    FreeAndNil(AdoConn);
  AdoConn := TAdoConnection.Create(Self);
  AdoConn.ConnectionString := ConnStr;
    AdoConn.LoginPrompt := false;
  SetConnAdoComponent(Self,AdoConn);
  AdoConn.Open;
  end;
end;
procedure SetConnAdoComponent(aSrc:TComponent; var AdoConn:TAdoConnection);
var
Ctrl : TComponent;
i : Integer;
begin
if (aSrc = Nil) then Exit;
if (aSrc.ComponentCount <= 0) then Exit;
for i:=0 to aSrc.ComponentCount-1 do begin
Ctrl := aSrc.Components[i];
if (Ctrl is TAdoQuery) then TAdoQuery(Ctrl).Connection := AdoConn;
if (Ctrl is TAdoTable) then TAdoTable(Ctrl).Connection := AdoConn;
if (Ctrl is TAdoDataset) then TAdoDataset(Ctrl).Connection := AdoConn;
end;
end
I Call Reconnect() from the Exception part in a TForm or TDataModule, AdoConn is the name of the TAdoConnection component and the ConnStr is the complete connectionstring used.
Except
On E:EOleException do begin
ReConnect(AdoConn,ConnStr);
end;
On E:Exception do begin
ReConnect(AdoConn,ConnStr);
end;
End;

Instead of destroying the TADOConnection your best option is to replace the internal TADOConnection.ConnectionObject with a new one. e.g.
uses ActiveX, ComObj, ADOInt;
function CreateADOConnectionObject: _Connection;
begin
OleCheck(CoCreateInstance(CLASS_Connection, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result));
end;
var
NewConnectionObject: _Connection;
ConnectionString: WideString;
begin
ConnectionString := ADOConnection1.ConnectionString;
NewConnectionObject := CreateADOConnectionObject;
NewConnectionObject.ConnectionString := ConnectionString;
ADOConnection1.Close;
// set the new connection object
ADOConnection1.ConnectionObject := NewConnectionObject;
ADOConnection1.Open;
end;
Setting ADOConnection1.ConnectionObject := NewConnectionObject will destroy the previous internal FConnectionObject and set a new connection object to be used by the TADOConnection object.
Also you need to handle the specific EOleException.ErrorCode (probably E_FAIL) at the time of the exception so that you sure you don't handle other exceptions which has nothing to do with your issue.
I did not try this with your specific scenario (SQL restart). I leave it up to you for testing.
EDIT: Tested with SQL Server 2014 and SQLOLEDB.1. My application connected to the SQL, and after restarting the SQL, I could not reproduce the described behavior "connection is dropped eternally". a Close/Open did the job, and the client re-connected.

Related

Delphi service app crashes at a random time

I have a Delphi Service app. Indy TCP server and many clients (up to 50), ADO connection to Firebird and simply network exchange. App randomly crashes (may be workin 7 days, may be 1 hour) with next event (for example):
Имя сбойного приложения: rollcontrol.exe, версия: 1.1.20.2, метка времени: 0x60acd5f2
Имя сбойного модуля: ntdll.dll, версия: 6.3.9600.19678, метка времени: 0x5e82c0f7
Код исключения: 0xc0000005
Смещение ошибки: 0x00058def
Идентификатор сбойного процесса: 0x4178
or:
Имя сбойного приложения: rollcontrol.exe, версия: 1.1.1.9, метка времени: 0x607b239c
Имя сбойного модуля: msvcrt.dll, версия: 7.0.9600.16384, метка времени: 0x52158ff5
Код исключения: 0xc0000005
Смещение ошибки: 0x00009e80
All jobs in app makes in anonimius threads or in tcp/ip connections threads. All code in each thread executed in try except statments. There no memory leaks or growing threads count. The main code of service thread very simple:
procedure TRollControl_Svc.ServiceExecute(Sender: TService);
begin
while not Terminated do
try
ServiceThread.ProcessRequests(False);
ServiceThread.Sleep(100);
except
on e : exception do LogException('ServiceExecute', E);
end;
end;
How I can handled this exception and prevent app crash? How it possible to crash service thread with two simple lines of code?
Thanks
UPDATE: Example of connections to DB:
function TRollControl_Svc.GetNodeIdByIP(ip: string): integer;
Var
SQLConnection : TADOConnection;
SQLQuery : TADOQuery;
Thread : TThread;
fResult : integer;
begin
fResult := 0;
try
Thread := nil;
Thread := TThread.CreateAnonymousThread(
procedure
begin
try
SQLConnection := nil;
SQLQuery := nil;
CoInitialize(nil);
SQLConnection := TADOConnection.Create(nil);
SQLConnection.ConnectionString := 'Provider=MSDASQL.1;Password=' + Psw + ';Persist Security Info=True;User ID=' + Usr + ';Data Source=' + Srv ;
SQLConnection.LoginPrompt := false;
SQLQuery := TADOQuery.Create(nil);
SQLQuery.Connection := SQLConnection;
SQLQuery.LockType := ltReadOnly;
try SQLConnection.Open; except SQLConnection.Open; end;
SQLConnection.BeginTrans;
SQLQuery.Close;
SQLQuery.SQL.Text := 'select nodes.* from nodes where nodes.ip = :ip';
SQLQuery.Parameters.ParamByName('ip').Value := ip;
try SQLQuery.Open; except SQLQuery.Open; end;
if SQLQuery.IsEmpty then exit;
fResult := SQLQuery.FieldByName('ID').AsInteger;
if SQLConnection.InTransaction then
SQLConnection.CommitTrans;
finally
TryFree(SQLQuery);
TryFree(SQLConnection);
CoUninitialize;
end;
end
);
Thread.FreeOnTerminate := false;
Thread.Start;
Thread.WaitFor;
finally
TryFree(Thread);
end;
result := fResult;
end;
Error Handling
This isn't an answer as to what is causing your problem, but I thought it probably wouldn't be clear in a comment.
In languages that support structured exception handling the language gives the programmer an opportunity to fail gracefully when things don't work. That's not how you are using it. From your example anonymous thread you have:
try SQLConnection.Open; except SQLConnection.Open; end;
So you are told that the connection can't be made and instead of responding to that situation you go ahead and attempt to connect again. There are lots of reasons why a connection may not work, some of those are transient so the attempt may work a little later but if you simply try doing it again without any pause it seems reasonable to expect it to fail again.
It's obviously important to catch errors, but you have to have appropriate failure paths.
I have no way of knowing if this is related to what's actually going wrong.
I found the reason. The problem was in the ADO source codes (Data.Win.ADODB.pas):
procedure RefreshFromOleDB;
var
I: Integer;
ParamCount: ULONG_PTR;
ParamInfo: PDBParamInfoArray;
NamesBuffer: POleStr;
Name: WideString;
Parameter: _Parameter;
Direction: ParameterDirectionEnum;
OLEDBCommand: ICommand;
OLEDBParameters: ICommandWithParameters;
CommandPrepare: ICommandPrepare;
begin
OLEDBCommand := (Command.CommandObject as ADOCommandConstruction).OLEDBCommand as ICommand;
OLEDBCommand.QueryInterface(ICommandWithParameters, OLEDBParameters);
OLEDBParameters.SetParameterInfo(0, nil, nil); // ----- Error here
if Assigned(OLEDBParameters) then
begin
ParamInfo := nil;
NamesBuffer := nil;
try
OLEDBCommand.QueryInterface(ICommandPrepare, CommandPrepare);
if Assigned(CommandPrepare) then CommandPrepare.Prepare(0);
if OLEDBParameters.GetParameterInfo(ParamCount, PDBPARAMINFO(ParamInfo), #NamesBuffer) = S_OK then
for I := 0 to ParamCount - 1 do//
begin
{ When no default name, fabricate one like ADO does }
if ParamInfo[I].pwszName = nil then
Name := 'Param' + IntToStr(I+1) else { Do not localize }
Name := ParamInfo[I].pwszName;
{ ADO maps DBTYPE_BYTES to adVarBinary }
if ParamInfo[I].wType = DBTYPE_BYTES then ParamInfo[I].wType := adVarBinary;
{ ADO maps DBTYPE_STR to adVarChar }
if ParamInfo[I].wType = DBTYPE_STR then ParamInfo[I].wType := adVarChar;
{ ADO maps DBTYPE_WSTR to adVarWChar }
if ParamInfo[I].wType = DBTYPE_WSTR then ParamInfo[I].wType := adVarWChar;
Direction := ParamInfo[I].dwFlags and $F;
{ Verify that the Direction is initialized }
if Direction = adParamUnknown then Direction := adParamInput;
Parameter := Command.CommandObject.CreateParameter(Name, ParamInfo[I].wType, Direction, ParamInfo[I].ulParamSize, EmptyParam);
Parameter.Precision := ParamInfo[I].bPrecision;
Parameter.NumericScale := ParamInfo[I].bScale;
//if ParamInfo[I].dwFlags and $FFFFFFF0 <= adParamSigned + adParamNullable + adParamLong then
Parameter.Attributes := ParamInfo[I].dwFlags and $FFFFFFF0; { Mask out Input/Output flags }
AddParameter.FParameter := Parameter;
end;
finally
if Assigned(CommandPrepare) then CommandPrepare.Unprepare;
if (ParamInfo <> nil) then GlobalMalloc.Free(ParamInfo);
if (NamesBuffer <> nil) then GlobalMalloc.Free(NamesBuffer);
end;
end;
end;
Line
OLEDBParameters.SetParameterInfo(0, nil, nil)
executed before
if Assigned(OLEDBParameters)
I moved this line after checking on nil and all working fine
I managed to isolate the problem. Errors occur periodically when working with ADO. If I try to use TADOQuery objects again, the application more susceptible to crashes. What I've done:
System.NeverSleepOnMMThreadContention: = false;
Significantly reduces errors when working with ADO
All uses of TADOQuery are single use.
For example it was:
for ii := 0 to SettingsXML.Root.NamedItem['sql_clear_base'].NamedItem['XML'].Count - 1 do
begin
try
SQLQuery.Close;
SQLQuery.SQL.Text := SettingsXML.Root.NamedItem['sql_clear_base'].NamedItem['XML'][ii].AsString;
SQLQuery.ExecSQL;
except
on e : exception do LogException('ClearBase', '', E);
end;
end;
Became:
for ii := 0 to SettingsXML.Root.NamedItem['sql_clear_base'].NamedItem['XML'].Count - 1 do
begin
SQLQuery := nil;
try
SQLQuery := TADOQuery.Create(nil);
SQLQuery.Connection := SQLConnection;
try
SQLQuery.Close;
SQLQuery.SQL.Text := SettingsXML.Root.NamedItem['sql_clear_base'].NamedItem['XML'][ii].AsString;
SQLQuery.ExecSQL;
except
on e : exception do LogException('ClearBase', '', E);
end;
finally
TryFree(SQLQuery);
end;
end;
I make self-control:
main procces started as windows service (process A)
process A starts a copy of itself as B
one per minutes A check if B alive and restart if not
one per minutes B check if A alive and restart if not
check - simple TCP packet and answer
For example:
TThread.CreateAnonymousThread(
procedure
var tcpClient : TidTCPClient;
begin
tcpClient := nil;
LastKeepAlive := Date + Time;
while ServerMode do
begin
try
if not Assigned(tcpClient) then
begin
tcpClient := TIdTCPClient.Create(nil);
tcpClient.Host := '127.0.0.1';
tcpClient.Port := RollControl_Svc.TCPServer.Bindings[0].Port;
tcpClient.Connect;
tcpClient.IOHandler.ReadTimeout := 1000;
end;
tcpClient.IOHandler.Write(START_PACKET + #0 + END_PACKET);
tcpClient.IOHandler.ReadString(3);
LastKeepAlive := Date + Time;
except
TryFree(tcpClient);
end;
sleep(15 * 1000);
end;
end).Start;
TThread.CreateAnonymousThread(
procedure
Var Res: TRequestResult;
begin
while ServerMode do
begin
if Date + Time - LastKeepAlive > OneMinute then
begin
Res.Clear('', '');
Res.Nodes_ID := -1;
Res.Data_In := 'KeepAlive';
Res.Data_Out := 'Exception: ExitProcess(1)';
try
Log(Res, true);
finally
ExitProcess(1);
end;
end;
sleep(1000);
end;
end).Start;
P.S. Local tests never crashed applications. The program simply proceed a million requests (connect, request, disconnect), there are no memory leaks or failures. On several clients servers are crashed. In the future I want to port to Lazarus to use ODBC directly insteed ADO

Exception caused by TDataSetProvider poUseQuoteChar and lowercase table names does not surface

The default TDataSetProvider.Options.poUseQuoteChar is true.
I was (again) bitten by this when my SQL statement used a lower case table name and my TClientDataSet.ApplyUpdates(0) did not do any updates without raising an exception.
In DataSnap.Provider the code in function TCustomResolver.InternalUpdateRecord(Tree: TUpdateTree): Boolean; traps the exception:
except
if ExceptObject is Exception then
begin
E := Exception(AcquireExceptionObject);
PrevErr.Free;
PrevErr := Err;
Err := (Tree.Source as IProviderSupportNG).PSGetUpdateException(E, PrevErr);
if HandleUpdateError(Tree, Err, FMaxErrors, FErrorCount) then
begin
Tree.Delta.UseCurValues := True;
Continue;
end else
break;
end else
raise;
end;
and I see that E.Message is
[FireDAC][Phys][FB]Dynamic SQL Error'#$D#$A'SQL error code = -204'#$D#$A'Table unknown'#$D#$A'tt_calendar'#$D#$A'At line 1, column 8
I have no ReconcileErrorHandler, and in the above code HandleUpdateError returns false, but for some reason the exception does not surface.
My setup is:
New events created in DevExpress TcxSchedulerStorage, connected to TDataSource -> TClientDataSet -> TDataSetProvider -> TFDQuery -> TFDConnection, in this case to a Firebird database. Everything default settings, simple select * from tablename in TFDQuery.SQL.Text,
using Delphi Tokyo 10.2.3.
Is there a single setting that I can change that would force the exception visible and that solves this once and for all (for any database type)?
I'm even willing to patch a Delphi file.
I have now 'solved' this with runtime code:
procedure TDMTT.DataModuleCreate(Sender: TObject);
var i: integer;
begin
for i := 0 to ComponentCount-1 do
if Components[i] is TDataSetProvider then
(Components[i] as TDataSetProvider).Options := (Components[i] as TDataSetProvider).Options - [poUseQuoteChar];
end;
but would prefer not having to think of this every time.
As Sertac Akyuz suggested, modifying the TDataSetProvider constructor is one option.
The drawback is that you are patching Delphi code, but I'm fine with that.
In Datasnap.Provider, add the indicated line:
constructor TDataSetProvider.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FResolveToDataSet := False;
UpdateMode := upWhereAll;
FDSWriter := nil;
FConstraints := True;
FRecordsSent := 0;
Options := Options - [poUseQuoteChar]; // Line added
end;

Multiple NT service owned by the same program in Delphi

I'm looking for Delphi sample code to develope a Win32 Windows service which can be installed many times (with different Name).
The idea is to have 1 exe and 1 registry key with 1 subkey for every service to be installed.
I use the exe to install/run many service, every service take his parameter from his registry subkey.
Does anyone have a sample code?
We've done this by creating a TService descendant and adding an 'InstanceName' property. This gets passed on the command line as something like ... instance="MyInstanceName" and gets checked for and set (if it exists) before SvcMgr.Application.Run.
eg
Project1.dpr:
program Project1;
uses
SvcMgr,
SysUtils,
Unit1 in 'Unit1.pas' {Service1: TService};
{$R *.RES}
const
INSTANCE_SWITCH = '-instance=';
function GetInstanceName: string;
var
index: integer;
begin
result := '';
for index := 1 to ParamCount do
begin
if SameText(INSTANCE_SWITCH, Copy(ParamStr(index), 1, Length(INSTANCE_SWITCH))) then
begin
result := Copy(ParamStr(index), Length(INSTANCE_SWITCH) + 1, MaxInt);
break;
end;
end;
if (result <> '') and (result[1] = '"') then
result := AnsiDequotedStr(result, '"');
end;
var
inst: string;
begin
Application.Initialize;
Application.CreateForm(TService1, Service1);
// Get the instance name
inst := GetInstanceName;
if (inst <> '') then
begin
Service1.InstanceName := inst;
end;
Application.Run;
end.
Unit1 (a TService descendant)
unit Unit1;
interface
uses
Windows, SysUtils, Classes, SvcMgr, WinSvc;
type
TService1 = class(TService)
procedure ServiceAfterInstall(Sender: TService);
private
FInstanceName: string;
procedure SetInstanceName(const Value: string);
procedure ChangeServiceConfiguration;
public
function GetServiceController: TServiceController; override;
property InstanceName: string read FInstanceName write SetInstanceName;
end;
var
Service1: TService1;
implementation
{$R *.DFM}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Service1.Controller(CtrlCode);
end;
procedure TService1.ChangeServiceConfiguration;
var
mngr: Cardinal;
svc: Cardinal;
newpath: string;
begin
// Open the service manager
mngr := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
if (mngr = 0) then
RaiseLastOSError;
try
// Open the service
svc := OpenService(mngr, PChar(Self.Name), SERVICE_CHANGE_CONFIG);
if (svc = 0) then
RaiseLastOSError;
try
// Change the service params
newpath := ParamStr(0) + ' ' + Format('-instance="%s"', [FInstanceName]); // + any other cmd line params you fancy
ChangeServiceConfig(svc, SERVICE_NO_CHANGE, // dwServiceType
SERVICE_NO_CHANGE, // dwStartType
SERVICE_NO_CHANGE, // dwErrorControl
PChar(newpath), // <-- The only one we need to set/change
nil, // lpLoadOrderGroup
nil, // lpdwTagId
nil, // lpDependencies
nil, // lpServiceStartName
nil, // lpPassword
nil); // lpDisplayName
finally
CloseServiceHandle(svc);
end;
finally
CloseServiceHandle(mngr);
end;
end;
function TService1.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TService1.ServiceAfterInstall(Sender: TService);
begin
if (FInstanceName <> '') then
begin
ChangeServiceConfiguration;
end;
end;
procedure TService1.SetInstanceName(const Value: string);
begin
if (FInstanceName <> Value) then
begin
FInstanceName := Value;
if (FInstanceName <> '') then
begin
Self.Name := 'Service1_' + FInstanceName;
Self.DisplayName := Format('Service1 (%s)', [FInstanceName]);
end;
end;
end;
end.
Usage:
Project1.exe /install
Project1.exe /install -instance="MyInstanceName"
Project1.exe /uninstall [-instance="MyInstanceName]
It doesn't actually do anything - it's up to you to write the start/stop server bits etc.
The ChangeServiceConfiguration call is used to update the real command line that the service manager calls when it starts up. You could just edit the registry instead but at least this is the 'proper' API way.
This allows any number of instances of the service to be run at the same time and they will appear in the service manager as 'MyService', 'MyService (Inst1)', 'MyService (AnotherInstance)' etc etc.
There's an issue on how services are implemented in Delphi that does not make easy to install a service more than once using a different name (see Quality Central report #79781). You may need to bypass the TService/TServiceApplication implementation.
To create the service using different names you can't simply use the /INSTALL command line parameter but you have to use the SCM API or one of its implementation (i.e. SC.EXE command line utility) or a setup tool.
To tell the service which key to read you can pass a parameter to the service on its command line (they have as well), parameters are set when the service is created.
Context: Service installed by running exename.exe /install as MyService. Service installed a second time as MyService2.
Delphi doesn't allow for a service in a single executable to be installed twice with different names. See QC 79781 as idsandon mentioned. The different name causes the service to "hang" (at least according to the SCM) in the "Starting" phase. This is because DispatchServiceMain checks for equality of the TService instance name and the name according to the SCM (passed in when it starts the service). When they differ DispatchServiceMain does not execute TService.Main which means the TService's start up code isn't executed.
To circumvent this (somewhat), call the FixServiceNames procedure just before the Application.Run call.
Limitations: alternate names must start with the original one. IE if the original name is MyService then you can install MyService1, MyServiceAlternate, MyServiceBoneyHead, etc.
What FixServiceNames does is look for all installed services, check ImagePath to see if the service is implemented by this executable and collect those in a list. Sort the list on installed ServiceName. Then check all TService descendents in SvcMgr.Application.Components. When a ServiceName is installed that starts with Component.Name (the original name of the service), then replace that with the one we got from the SCM.
procedure FixServiceNames;
const
RKEY_SERVICES = 'SYSTEM\CurrentControlSet\Services';
RKEY_IMAGE_PATH = 'ImagePath';
RKEY_START = 'Start';
var
ExePathName: string;
ServiceNames: TStringList;
Reg: TRegistry;
i: Integer;
ServiceKey: string;
ImagePath: string;
StartType: Integer;
Component: TComponent;
SLIndex: Integer;
begin
ExePathName := ParamStr(0);
ServiceNames := TStringList.Create;
try
Reg := TRegistry.Create(KEY_READ);
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
// Openen registry key with all the installed services.
if Reg.OpenKeyReadOnly(RKEY_SERVICES) then
begin
// Read them all installed services.
Reg.GetKeyNames(ServiceNames);
// Remove Services whose ImagePath does not match this executable.
for i := ServiceNames.Count - 1 downto 0 do
begin
ServiceKey := '\' + RKEY_SERVICES + '\' + ServiceNames[i];
if Reg.OpenKeyReadOnly(ServiceKey) then
begin
ImagePath := Reg.ReadString(RKEY_IMAGE_PATH);
if SamePath(ImagePath, ExePathName) then
begin
// Only read 'Start' after 'ImagePath', the other way round often fails, because all
// services are read here and not all of them have a "start" key or it has a different datatype.
StartType := Reg.ReadInteger(RKEY_START);
if StartType <> SERVICE_DISABLED then
Continue;
end;
ServiceNames.Delete(i);
end;
end;
end;
finally
FreeAndNil(Reg);
end;
// ServiceNames now only contains enabled services using this executable.
ServiceNames.Sort; // Registry may give them sorted, but now we are sure.
if ServiceNames.Count > 0 then
for i := 0 to SvcMgr.Application.ComponentCount - 1 do
begin
Component := SvcMgr.Application.Components[i];
if not ( Component is TService ) then
Continue;
// Find returns whether the string is found and reports through Index where it is (found) or
// where it should be (not found).
if ServiceNames.Find(Component.Name, SLIndex) then
// Component.Name found, nothing to do
else
// Component.Name not found, check whether ServiceName at SLIndex starts with Component.Name.
// If it does, replace Component.Name.
if SameText(Component.Name, Copy(ServiceNames[SLIndex], 1, Length(Component.Name))) then
begin
Component.Name := ServiceNames[SLIndex];
end
else
; // Service no longer in executable?
end;
finally
FreeAndNil(ServiceNames);
end;
end;
Note: SO pretty printer gets confused at the "ServiceKey := '\' + RKEY_SERVICES + '\' + ServiceNames[i];" line, Delphi (2009) has no issues with it.

Delphi: Clientdataset: EDatabaseError on .Open; with ProviderName set

So I'm having this code that processes what the client sends on a pattern. If he sends 'getBENUds', the server sends the DataSet for this table back using the SaveToString method.
Then, this is sent to the client. (I'm using Synapse).
procedure TTCPSocketThrd.Execute;
var s: String;
strm: TMemoryStream;
ADO_CON: TADOConnection;
ADO_QUERY: TADOQuery;
DS_PROV: TDataSetProvider;
DS_CLIENT: TClientDataSet;
begin
CoInitialize(nil);
Sock := TTCPBlockSocket.Create;
try
Sock.Socket := CSock;
Sock.GetSins;
with Sock do
begin
repeat
if terminated then break;
s := RecvTerminated(60000,'|');
if s = 'getBENUds' then
begin
//ini ADO_CON
ADO_CON := TADOConnection.Create(Form1);
ADO_CON.ConnectionString := 'not for public';
ADO_CON.LoginPrompt := false;
ADO_CON.Provider := 'SQLOLEDB.1';
ADO_CON.Open;
//ini ADO_QUERY
ADO_QUERY := TADOQuery.Create(ADO_CON);
ADO_QUERY.Connection := ADO_CON;
//ini DS_PROV
DS_PROV := TDataSetProvider.Create(ADO_CON);
DS_PROV.DataSet := ADO_QUERY;
//ini DS_CLIENT
DS_CLIENT := TClientDataSet.Create(ADO_CON);
DS_CLIENT.ProviderName := 'DS_PROV';
//SQLQUERY Abfrage
ADO_QUERY.SQL.Clear;
ADO_QUERY.SQL.Add('SELECT * FROM BENU');
ADO_QUERY.Open;
//DSCLIENTDATASET bauen
strm := TMemoryStream.Create;
DS_CLIENT.Open;
DS_CLIENT.SaveToStream(strm);
end
else if s = 'getBESTEds' then
...
The line it says: DS_CLIENT.Open an exception is thrown:
An exception has been thrown: class EDatabaseError. Text: 'missing data-provider or data package'.
The data-provider has been set as can be seen above to 'DS_PROV', so it has to be the missing data package.
But shouldn't the ClientDataSet get its data from the DataSetProvider which in turn gets it from the ADOQuery that gets the data from the database?
This is as far as I get with my level of knowledge. I hope its not too difficult, because in my eyes, everything I did was correct.
Use
DS_CLIENT.SetProvider(DS_PROV);
or after DS_PROV creation: (at this time your component has really no name)
DS_PROV.name := 'DS_PROV';

How can I display Crystal XI reports inside a Delphi 2007 application?

The most recent Crystal XI component for Delphi was released for Delphi 7. That VCL component compiles in D2007, but gives me errors at runtime. What is the best way to display a database-connected Crystal Report in a Delphi 2007 application?
This is the solution I've found, using ActiveX:
First, register the Active X control like this:
In Delphi, choose Component -> Import Component
Click on "Type Library", click Next
Choose "Crystal ActiveX Report Viewer Library 11.5"
Pick whatever Palette Page you want (I went with "Data Access")
Choose an import location
Exit out of the wizard
Add the location you chose to your project Search Path
Now this code should work:
...
uses
CrystalActiveXReportViewerLib11_5_TLB, OleAuto;
...
procedure TForm1.Button1Click(Sender: TObject);
var
cry : TCrystalActiveXReportViewer;
oRpt, oApp : variant;
i : integer;
frm : TForm;
begin
cry := TCrystalActiveXReportViewer.Create(Self);
oApp := CreateOleObject('CrystalRuntime.Application');
oRpt := oApp.OpenReport('c:\my_report.rpt',1);
for i := 1 to oRpt.Database.Tables.Count do begin
oRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] := 'username';
oRpt.Database.Tables[i].ConnectionProperties.Item['Password'] := 'password';
end;
frm := TForm.Create(Self);
try
cry.Parent := frm;
cry.Align := alClient;
cry.ReportSource := oRpt;
cry.ViewReport;
frm.Position := poOwnerFormCenter;
frm.ShowModal;
finally
FreeAndNil(frm);
end; //try-finally
end;
procedure TForm1.btnExportClick(Sender: TObject);
var
cry : TCrystalActiveXReportViewer;
oRpt, oApp : variant;
i : integer;
begin
//Export the report to a file
cry := TCrystalActiveXReportViewer.Create(Self);
oApp := CreateOleObject('CrystalRuntime.Application');
oRpt := oApp.OpenReport(c_DBRpt,1);
for i := 1 to oRpt.Database.Tables.Count do begin
oRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] := 'username';
oRpt.Database.Tables[i].ConnectionProperties.Item['Password'] := 'password';
end;
oRpt.ExportOptions.FormatType := 29; //excel 8
oRpt.ExportOptions.DiskFileName := 'c:\output.xls';
oRpt.ExportOptions.DestinationType := 1; //file destination
//Export(False) => do NOT prompt.
//Export(True) will give runtime prompts for export options.
oRpt.Export(False);
end;
If you use this method, then this (rather dense) reference will be helpful, especially since Intellisense doesn't work on Ole objects like these.
Edit: The original link to the reference broke, so I changed it to point to a new one (valid as of Dec 15 2009). If that new one breaks, then Google should be able to find it.
I know it's not your question and it might not be an acceptable answer at all in your situation, but I have found FastReports to be clearly superior to Crystal for my purposes. It's lighter weight, includes a real scripting language, incorporates event handling, can make calls into your native code for information and updates and does not require an ActiveX connection. I can export my reports into sharp looking PDF files or Excel spreadsheets and several other formats. The quality of the output adds to the overall experience users get from my application. I could go on, but if it's off topic for you, it won't be helpful.
For the sake of anyone else who can use it, here is a complete class that gives a pleasant wrapper around these vile Crystal interactions. It works for me about 80% of the time, but I suspect a lot of this stuff is very dependent on the specific platform on which it runs. I'll post improvements as I make them.
Somebody at Business Objects should really take a hard look at this API. It sucks pretty badly.
{
Class to facilitate the display of Crystal 11 Reports.
The Crystal 11 VCL component does not seem to work with Delphi 2007.
As a result, we have to use ActiveX objects, which make deployment messy.
This class is similar to CrystalReporter, but it works for Crystal 11.
However, it lacks some of the features of the old CrystalReporter.
Refer to the crystal reports activex technical reference to duplicate the
missing functionality.
Example usage is at the bottom of this unit.
//}
unit CrystalReporter11;
interface
uses
CrystalActiveXReportViewerLib11_5_TLB, OleAuto, Classes, Controls;
type
TCryExportFormat = (
XLS
,PDF
);
type
TCrystalReporter11 = class
private
FCryRpt : TCrystalActiveXReportViewer;
FRpt, FApp : variant;
FReportFile, FUsername, FPassword, FServer, FFilters : string;
FOwner : TComponent;
procedure SetLoginInfo(const username, password, server : string);
function GetFilterConds: string;
procedure SetFilterConds(const Value: string);
public
property FilterConditions : string read GetFilterConds write SetFilterConds;
procedure ExportToFile(ExportFileName : string;
FileExportFmt : TCryExportFormat; PromptForOptions : boolean);
procedure Display;
constructor Create(AOwner : TComponent; ReportFile : string); overload;
constructor Create(AOwner : TComponent; ReportFile,
Username, Password, Server : string); overload;
end;
implementation
uses
SysUtils, Forms;
const
//these are taken from pgs 246 and 247 of the technical reference
c_FmtCode_Excel = 29;
c_FmtCode_PDF = 31;
constructor TCrystalReporter11.Create(AOwner: TComponent; ReportFile: string);
begin
inherited Create;
try
FReportFile := ReportFile;
if FileExists(FReportFile) then begin
FOwner := AOwner;
FCryRpt := TCrystalActiveXReportViewer.Create(AOwner);
FApp := CreateOleObject('CrystalRuntime.Application');
FRpt := FApp.OpenReport(FReportFile,1);
FFilters := FRpt.RecordSelectionFormula;
end
else begin
raise Exception.Create('Report file ' + ReportFile + ' not found!');
end;
except on e : exception do
raise;
end; //try-except
end;
constructor TCrystalReporter11.Create(AOwner: TComponent; ReportFile, Username,
Password, Server: string);
begin
Create(AOwner,ReportFile);
FUsername := Username;
FPassword := Password;
FServer := Server;
SetLoginInfo(FUsername,FPassword,FServer);
end;
procedure TCrystalReporter11.Display;
var
rptForm : TForm;
begin
SetLoginInfo(FUsername,FPassword,FServer);
FCryRpt.ReportSource := FRpt;
rptForm := TForm.Create(FOwner);
try
FCryRpt.Parent := rptForm;
FCryRpt.Align := alClient;
FCryRpt.ViewReport;
rptForm.Position := poOwnerFormCenter;
rptForm.WindowState := wsMaximized;
rptForm.Caption := ExtractFileName(FReportFile);
rptForm.ShowModal;
finally
FreeAndNil(rptForm);
end; //try-finally
end;
procedure TCrystalReporter11.ExportToFile(ExportFileName : string;
FileExportFmt : TCryExportFormat; PromptForOptions : boolean);
begin
case FileExportFmt of
XLS : FRpt.ExportOptions.FormatType := c_FmtCode_Excel;
PDF : FRpt.ExportOptions.FormatType := c_FmtCode_PDF;
end; //case
FRpt.ExportOptions.DiskFileName := ExportFileName;
FRpt.ExportOptions.DestinationType := 1; //file destination
FCryRpt.ReportSource := FRpt;
FRpt.Export(PromptForOptions);
end;
function TCrystalReporter11.GetFilterConds: string;
begin
Result := FFilters;
end;
procedure TCrystalReporter11.SetFilterConds(const Value: string);
begin
FFilters := Value;
if 0 < Length(Trim(FFilters)) then begin
FRpt.RecordSelectionFormula := Value;
end;
end;
procedure TCrystalReporter11.SetLoginInfo(const username, password,
server : string);
var
i : integer;
begin
//set user name and password
//crystal only accepts these values if they are CONST params
for i := 1 to FRpt.Database.Tables.Count do begin
FRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] := username;
FRpt.Database.Tables[i].ConnectionProperties.Item['Password'] := password;
try
{
Some reports use direct connections, and others use an ODBC Data Source.
Crystal XI uses a different label to refer to the database name in each
method.
I don't know how to determine in advance which method is being used, so:
First, we try the direct connection.
If that fails, we try the "data source" method.
Reference: "Crystal Reports XI Technical Reference", pages 41 thru 46;
"Common ConnectionProperties"
}
FRpt.Database.Tables[i].ConnectionProperties.Item['Server'] := server;
except on E: Exception do
FRpt.Database.Tables[i].ConnectionProperties.Item['Data Source'] := server;
end;
end;
end;
{
Example usage:
procedure TForm1.btnShowRptDBClick(Sender: TObject);
var
cry : TCrystalReporter11;
begin
cry := TCrystalReporter11.Create(Self,'c:\my_report.rpt','username',
'password','server.domain.com');
try
cry.Display;
finally
FreeAndNil(cry);
end;
end;
}
end.
I too have been disappointed with the lack of effort by Crystal Reports with respect to application integration. I use the RDC, and from what I understand this is being deprecated and emphasis is being placed on .Net.
My application has these files in the uses clause:
CRRDC, CRAXDRT_TLB,
It works ok. The because drawback is parameter passing. In my option the parameter dialog boxes which come with the viewer are terrible. So I use my own Delphi application to prompt for parameters and pass them to the report.
Here is a bit simpler and clean class which solves the problem very nicely:
Unit CrystalReports;
uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.OleCtrls, ActiveX, ComObj, Data.DB, Data.Win.ADODB,
CrystalActiveXReportViewerLib11_TLB, Vcl.OleServer, CrystalReportsControllersLib_TLB;
type
TCrystalReportForm = class(TForm)
CRV: TCrystalActiveXReportViewer;
procedure DisplayReport;
private
{ Private declarations }
public
{Public declarations }
ReportName : WideString;
ReportCaption : String;
ReportSelectionFormula : WideString;
end;
var
CRXIRuntime : Variant;
implementation
{$R *.dfm}
procedure TCrystalReportForm.DisplayReport;
var
CrystalReport : variant;
i : integer;
begin
CrystalReport := CRXIRuntime.OpenReport(ReportName);
for i := 1 to CrystalReport.Database.Tables.Count do begin
CrystalReport.Database.Tables[1].ConnectionProperties.Item['User ID'] := 'user';
CrystalReport.Database.Tables[1].ConnectionProperties.Item['Password'] := 'password';
end;
CrystalReport.FormulaSyntax := 0;
Caption := ReportCaption;
CrystalReport.RecordSelectionFormula := ReportSelectionFormula;
CRV.Align := alClient;
CRV.ReportSource := CrystalReport;
WindowState := wsMaximized;
CRV.ViewReport;
ShowModal;
end;
begin
CRXIRuntime := CreateOleObject('CrystalRuntime.Application');
end.

Resources