What is supposed to be a simple TCP Client Server,, STUCK - delphi

I am stuck. I have previously used the server and client sockets (non-indy) and they were nicely asynchronous and easy to use. I am trying to make a simple client server. Once connected to a client the server sends out small blocks of data on a timer event. The time is 77ms and the blocks of data are approximately 100 bytes.
I have tried my best to figure out indy from bits here and there and people's examples. (Obviously there wouldn't be so much about it on the web if people could get it working quickly). Below I extracted the relevant sections from my client and server.
I have the ip set to localhost and i am trying to first debug the server side. I start the client, I start the server in the debugger. the client connects and I can put a break point at the onconnect event and the program goes there. Now what is supposed to happen is the timer is enabled and then every time it trips I send my block of data. (this doesn't happen at least in the debugger)
The compiler forced me to add an onexecute event for the server and I don't know what to do with it now. I tried a blank event and I tried a dummy readln.
On the Client side. I copied a read thread from elsewhere once there is a connection it is supposed to start and read the data blocks as they occur. Right now I am not doing any error checking or automatic detection of loss of connection and attempted re-connection.
I would certainly appreciate any help , both in getting this working and also in hints on how to handle the corner cases.
thanks
robert
Server side this is supposed to send a block of data every 77 ms
....................................................................................
type
TForm8 = class(TForm)
SendButton: TButton;
SendWaveFormTimer: TTimer;
IdUDPClient1: TIdUDPClient;
IdTCPServer1: TIdTCPServer;
procedure SendWaveFormTimerTimer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure IdTCPServer1Connect(AContext: TIdContext);
procedure IdTCPServer1Execute(AContext: TIdContext);
private
connectedto:TIdContext;
{ Private declarations }
public
{ Public declarations }
end;
var
Form8: TForm8;
procedure TForm8.IdTCPServer1Connect(AContext: TIdContext);
// this is not in the main ui thread and can't call showmessage
begin
connectedto:=acontext; // save the connected TIDcontext for use elsewhere?
self.SendWaveFormTimer.Enabled:=true; {set the send timer}
end;
procedure TForm8.SendWaveFormTimerTimer(Sender: TObject);
var tempbyte:tidbytes;
begin
tempbyte:=RawToBytes(WaveFormSample,sizeof(TWaveFormSample));
form8.connectedto.Connection.IOHandler.Write(tempbyte, sizeof(TWaveFormSample));
end;
procedure TForm8.IdTCPServer1Execute(AContext: TIdContext);
// again this in second thread
var recv:string;
begin
//I don't know what to put here I am not expecting anything right now
recv := AContext.Connection.Socket.ReadLn;
end;
Client Side
type
TForm3 = class(TForm)
IdUDPServer1: TIdUDPServer;
Label1: TLabel;
IdTCPClient1: TIdTCPClient;
Button1: TButton;
procedure IdUDPServer1UDPRead(AThread: TIdUDPListenerThread;
const AData: TIdBytes; ABinding: TIdSocketHandle);
procedure FormActivate(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TReadingThread = class(TThread)
protected
FConn: TIdTCPConnection;
procedure Execute; override;
public
constructor Create(ACon: TIdTCPConnection); reintroduce;
end;
type
TMyNotify = class(TidNotify)
private
recvd_block:TWaveFormSample;
protected
procedure DoNotify; override;
end;
var
Form3: TForm3;
Udp_message_ID:tmessageid;
UDp_WaveFormSample:twaveformsample;
ReadingThread: TReadingThread = nil;
implementation
constructor TReadingThread.Create(ACon: TIdTCPConnection);
begin
FConn := ACon;
inherited Create(False);
end;
procedure TForm3.IdTCPClient1Connected(Sender: TObject);
begin
readingthread.Create(IdTCPClient1);
readingthread.execute; //this must be wrong
end;
procedure tform3.Button1Click(Sender: TObject);
begin
IdTCPClient1.Connect; //this works on the other end
end;
procedure TReadingThread.Execute;
var
TCP_TWaveFormSample:twaveformsample;
AData: TIdBytes;
data_rdy:boolean;
MyNotify: TMyNotify;
begin
MyNotify := TMyNotify.Create;
MyNotify.Notify;
data_rdy:=false;
while not Terminated do
begin
FConn.IOHandler.CheckForDataOnSource(1000);
FConn.IOHandler.CheckForDisconnect;
if not FConn.IOHandler.InputBufferIsEmpty and not data_rdy then
begin
fconn.iohandler.ReadBytes(Adata,sizeof(TWaveFormSample));
Idglobal.BytesToRaw(AData, TCP_message_ID, sizeof(tmessageid)); //global load
Idglobal.BytesToRaw(AData, TCP_TWaveFormSample, sizeof(twaveformsample));
data_rdy:=true;
end; // data
If not Drawing and data_rdy then //make sure the other thread can take it
begin
MyNotify.recvd_block :=Udp_TWaveFormSample;
MyNotify.Notify;
data_rdy:=false;
end;
end;
end ;
procedure TMyNotify.DoNotify;
begin
waveformunit.writenewdata (recvd_block.WaveformIndex,recvd_block.WaveformData,
samples_per_send);
drawing:=false;
end;

TIdTCPServer is a multi-threaded component. Its events are fired in worker threads. Your timer does not work because TTimer is a message-based timer and there is no message loop in those threads to service it.
You are supposed to do your I/O work with the client in the server's OnExecute event, which is fired in the thread that manages the client connection, not in the main UI thread.
Everything you have shown on the server side is all wrong. Try something more like this:
interface
...
type
TForm8 = class(TForm)
IdTCPServer1: TIdTCPServer;
procedure FormCreate(Sender: TObject);
procedure IdTCPServer1Connect(AContext: TIdContext);
procedure IdTCPServer1Execute(AContext: TIdContext);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form8: TForm8;
implementation
type
TMyContext = class(TIdServerContext)
LastSend: TIdTicks;
end;
procedure TForm8.FormCreate(Sender: TObject);
begin
IdTCPServer1.ContextClass := TMyContext;
end;
procedure TForm8.IdTCPServer1Connect(AContext: TIdContext);
begin
TMyConext(AContext).LastSend := Ticks64;
end;
procedure TForm8.IdTCPServer1Execute(AContext: TIdContext);
var
Ctx: TMyContext;
temp: TIdBytes;
begin
Ctx := TMyContext(AContext);
if GetElapsedTicks(Ctx.LastSend) >= 77 then
begin
// grab the latest sample and send it...
temp := RawToBytes(Waveformsample, sizeof(TWaveFormSample));
AContext.Connection.IOHandler.Write(temp);
Ctx.LastSend := Ticks64;
end;
// in case the client sends something, just ignore it for now...
AContext.Connection.IOHandler.InputBuffer.Clear;
Sleep(0);
end;
That being said, since your server is expecting only 1 client, you might consider using TIdSimpleServer instead, which is not multi-threaded at all, so you can run it from the main UI thread if you want to. But if you want to service multiple clients, stick with TIdTCPServer.
On the client side, everything you have shown is also all wrong. You are misusing TThread, the IOHandler, and TIdNotify. Try something more like this:
interface
...
type
TForm3 = class(TForm)
IdTCPClient1: TIdTCPClient;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form3: TForm3;
implementation
type
TReadingThread = class(TThread)
protected
FConn: TIdTCPConnection;
procedure Execute; override;
public
constructor Create(ACon: TIdTCPConnection); reintroduce;
end;
TMyNotify = class(TIdNotify)
private
recvd_block: TWaveFormSample;
protected
procedure DoNotify; override;
end;
var
ReadingThread: TReadingThread = nil;
constructor TReadingThread.Create(ACon: TIdTCPConnection);
begin
FConn := ACon;
inherited Create(False);
end;
procedure TForm3.IdTCPClient1Connected(Sender: TObject);
begin
ReadingThread := TReadingThread.Create(IdTCPClient1);
end;
procedure Tform3.Button1Click(Sender: TObject);
begin
IdTCPClient1.Connect;
end;
procedure TReadingThread.Execute;
var
WaveFormSample: TWaveFormSample;
AData: TIdBytes;
MyNotify: TMyNotify;
begin
while not Terminated do
begin
FConn.IOHandler.ReadBytes(AData, sizeof(TWaveFormSample), False);
BytesToRaw(AData, WaveFormSample, sizeof(TWaveFormSample));
MyNotify := TMyNotify.Create;
MyNotify.recvd_block := WaveFormSample;
MyNotify.Notify;
end;
end;
procedure TMyNotify.DoNotify;
begin
// use recvd_block as needed...
end;

Related

Service application in Delphi

I am struggling with service application in Delphi but no major success so far. I tried to recreate this project, but it doesn't seem to work properly. File is created, but date and time aren't added to file every 10 seconds. I also don't see a message popping up from my ShowMessage. I successfully install and start service application.
Here is my code:
unit TMS;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs,
Vcl.ExtCtrls;
type
TWorkflow = class(TService)
Timer1: TTimer;
procedure ServiceExecute(Sender: TService);
procedure Timer1Timer(Sender: TObject);
procedure ServiceBeforeInstall(Sender: TService);
private
{ Private declarations }
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
Workflow: TWorkflow;
implementation
{$R *.dfm}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Workflow.Controller(CtrlCode);
end;
function TWorkflow.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TWorkflow.ServiceBeforeInstall(Sender: TService);
begin
Interactive := True;
end;
procedure TWorkflow.ServiceExecute(Sender: TService);
begin
while not Terminated do
begin
ServiceThread.ProcessRequests(True);
end;
end;
procedure TWorkflow.Timer1Timer(Sender: TObject);
const
FileName = 'D:\Projekti\EMBRACADERO\TMSWorkflow\Win32\Debug\Log.txt';
var
F : TextFile;
begin
AssignFile(F, FileName);
if FileExists(FileName) then
Append(F)
else
Rewrite(F);
Writeln(F, DateTimeToStr(Now), ' ', DiskFree(0));
ShowMessage(DateTimeToStr(Now));
CloseFile(F);
end;
end.
Could somebody give me an example of a service application with threads maybe or service with visual components included?
UPDATE1:
It is working with following code for inserting some data in database every 3 seconds.
private
thread : TThread;
procedure TWorkflow.InsertInDatabase;
begin
FDTransaction1.StartTransaction;
try
FDQuery1.Execute;
FDTransaction1.Commit;
except
FDTransaction1.Rollback;
end;
end;
procedure TWorkflow.ServiceExecute(Sender: TService);
begin
while not Terminated do
begin
ServiceThread.ProcessRequests(False);
InsertInDatabase();
thread.sleep(3000);
end;
end;
procedure TWorkflow.ServiceStart(Sender: TService; var Started: Boolean);
begin
thread := TThread.Create;
end;
procedure TWorkflow.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
FreeAndNil(thread);
end;
The TTimer code you showed is fine (though your OnExecute event is redundant can should be completely removed), except for the call to ShowMessage(), which you cannot use in a service at all (the TService.Interactive property has no effect on Windows Vista+). If you must display a popup message box from a service (which you should strive not to), you must use the Win32 API MessageBox() with the MB_SERVICE_NOTIFICATION flag specified, or use WTSSendMessage() instead. Otherwise, you have to delegate any UI to a separate non-service process that the service spawns and/or communicates with as needed.
Your TThread code, on the other hand, is completely wrong. It should be more like this instead:
unit TMS;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.SvcMgr;
type
TWorkflowThread = class(TThread)
protected
procedure Execute; override;
end;
TWorkflow = class(TService)
FDTransaction1: TFDTransaction;
FDQuery1: TFDQuery;
procedure ServiceStart(Sender: TService; var Started: Boolean);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceShutdown(Sender: TService);
private
{ Private declarations }
thread: TWorkflowThread;
procedure InsertInFile;
procedure InsertInDatabase;
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
Workflow: TWorkflow;
implementation
{$R *.dfm}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Workflow.Controller(CtrlCode);
end;
function TWorkflow.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TWorkflow.InsertInFile;
const
FileName = 'D:\Projekti\EMBRACADERO\TMSWorkflow\Win32\Debug\Log.txt';
var
F : TextFile;
begin
try
AssignFile(F, FileName);
try
if FileExists(FileName) then
Append(F)
else
Rewrite(F);
Writeln(F, DateTimeToStr(Now), ' ', DiskFree(0));
//ShowMessage(DateTimeToStr(Now));
finallly
CloseFile(F);
end;
except
end;
end;
procedure TWorkflow.InsertInDatabase;
begin
try
FDTransaction1.StartTransaction;
try
FDQuery1.Execute;
FDTransaction1.Commit;
except
FDTransaction1.Rollback;
end;
except
end;
end;
procedure TWorkflow.ServiceStart(Sender: TService; var Started: Boolean);
begin
thread := TWorkflowThread.Create(False);
Started := True;
end;
procedure TWorkflow.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
ServiceShutdown(Sender);
Stopped := True;
end;
procedure TWorkflow.ServiceShutdown(Sender: TService);
begin
if Assigned(thread) then
begin
thread.Terminate;
while WaitForSingleObject(thread.Handle, WaitHint-100) = WAIT_TIMEOUT do
ReportStatus;
FreeAndNil(thread);
end;
end;
procedure TWorkflowThread.Execute;
begin
while not Terminated do
begin
Workflow.InsertInFile;
Workflow.InsertInDatabase;
TThread.Sleep(3000);
end;
end;
end.
Your timer code will not execute because timers rely on a window handle and message pump that TService does not provide. Furthermore, TTimer is not thread-safe because in uses the VCL's AllocateHwnd() function which is not thread-safe and should not be used outside the context of the main thread. Generally, when writing service applications you would spawn a worker thread to perform the main logic.
If you need a thread-safe timer, I would suggest you use a different timer mechanism, such as WaitForSingleObject()
Assitionally, services should not contain any visual controls as they should not interact with the desktop at all.
Could somebody give me an example of service application with threads.
If your code is doing all its work in a thread, you are almost done.
Just start your thread in the service start event. For debugging, run the thread in a small (console) program.
Instead of a timer, let your main thread sleep for a while.

Drag and Drop files to Delphi form not working

I've tried to accept files that are dragged and dropped to a Form from the File Explorer but it doesn't work. My WM_DROPFILES handler is never called. I'm running Windows 8 if that makes any difference.
Here's a simple example of what I do (I just have a TMemo on the form):
type
TForm1 = class(TForm)
Memo1: TMemo;
private
{ Private declarations }
procedure WMDROPFILES(var msg : TWMDropFiles) ; message WM_DROPFILES;
procedure CreateWnd; override;
procedure DestroyWnd; override;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TForm1 }
procedure TForm1.CreateWnd;
begin
inherited;
DragAcceptFiles(Handle, True);
end;
procedure TForm1.DestroyWnd;
begin
inherited;
DragAcceptFiles(Handle, false);
end;
procedure TForm1.WMDROPFILES(var msg: TWMDropFiles);
var
i, fileCount: integer;
fileName: array[0..MAX_PATH] of char;
begin
fileCount:=DragQueryFile(msg.Drop, $FFFFFFFF, fileName, MAX_PATH);
for i := 0 to fileCount - 1 do
begin
DragQueryFile(msg.Drop, i, fileName, MAX_PATH);
Memo1.Lines.Add(fileName);
end;
DragFinish(msg.Drop);
end;
Most likely you are running your application elevated. Probably because you are running Delphi elevated. In Vista and later, low privilege processes cannot send messages to higher privilege processes. This MSDN blog explains more.
If you are running your Delphi IDE elevated, I urge you to stop doing so. There's very seldom a need to do so for standard desktop application development.
As Remy points out, your DestroyWnd is incorrect. You are destroying the window handle before calling DragAcceptFiles. Simply reverse the order. Personally I'd use WindowHandle in both CreateWnd and DestroyWnd. The Handle property creates the window handle if it is not assigned and so masks such errors.
procedure TForm1.CreateWnd;
begin
inherited;
DragAcceptFiles(WindowHandle, True);
end;
procedure TForm1.DestroyWnd;
begin
DragAcceptFiles(WindowHandle, false);
inherited;
end;

SAPI with Dephi: Async speech doesn't work

The following works perfectly (Delphi 7):
procedure TMainForm.SayIt(s:string); // s is the string to be spoken
var
voice: OLEVariant;
begin
memo1.setfocus;
voice := CreateOLEObject ('SAPI.SpVoice');
voice.Voice := voice.GetVoices.Item(combobox1.ItemIndex); // current voice selected
voice.volume := tbVolume.position;
voice.rate := tbRate.position;
voice.Speak (s, SVSFDefault);
end;
The above works in "sync" mode (SVSFDefault flag), but if I change the flag to SVSFlagsAsync in an attempt to play the sound in async mode, no soud is produced. No error messages are given, but nothing is played on the speakers.
What might the problem be? I have the SpeechLib_TLB unit in Delphi's Imports folder.
EDIT: This is in Windows XP
Thanks,
Bruno.
When you uses the SVSFlagsAsync flag, the voice stream is queued in an internal buffer and stay waiting to be executed by the speech service, So I think which you issue is related to the lifetime of the voice object, because is a local variable , the instance is destroyed before to execute the sound.
As workaround you can wait for the sound, using the WaitUntilDone method
voice.Speak (s, SVSFlagsAsync);
repeat Sleep(100); until voice.WaitUntilDone(10);
or declare the voice variable in you form definition.
TMainForm = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
voice: OLEVariant;
procedure SayIt(const s:string);
end;
var
MainForm: TMainForm;
implementation
uses
ComObj;
{$R *.dfm}
procedure TMainForm.SayIt(const s:string); // s is the string to be spoken
const
SVSFDefault = 0;
SVSFlagsAsync = 1;
SVSFPurgeBeforeSpeak= 2;
begin
memo1.setfocus;
voice.Voice := voice.GetVoices.Item(combobox1.ItemIndex); // current voice selected
voice.volume := tbVolume.position;
voice.rate := tbRate.position;
voice.Speak (s, SVSFlagsAsync {or SVSFPurgeBeforeSpeak});
end;
procedure TMainForm.Button1Click(Sender: TObject);
begin
SayIt('Hello');
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
voice := CreateOLEObject('SAPI.SpVoice');
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
voice := Unassigned;
end;
end.
As additional note, since you are using late binding you don't need the SpeechLib_TLB unit.

Why a published Int64 property-writer method wouldn't be called - Component streaming

Here is a simple test demonstrating the issue I encounter in a project, using Delphi 2007. I use a TComponent class for storing various states of a component. But the Int64 property writer methods are never called (only the destination field is set). So it's not possible to rely on the writer to update a GUI a TList or such things...
For example:
TTestClass = Class(TComponent)
Private
Fb: Int64;
Fa: Integer;
Procedure SetFa(Const Value: Integer);
Procedure SetFb(Const Value: Int64);
Published
Property a: Integer Read Fa Write SetFa;
Property b: Int64 Read Fb Write SetFb;
Public
Procedure SaveInstance(Var Str: TStream);
Procedure LoadInstance(Var Str: TStream);
Procedure ReallyLoadInstance(Var Str: TStream);
Procedure Assign(Source: TPersistent); Override;
End;
TForm1 = Class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Procedure Button1Click(Sender: TObject); // test: 1st step, save the class
Procedure Button2Click(Sender: TObject); // test: 2nd step, try and fail to reload
Procedure Button3Click(Sender: TObject); // test: 3rd step, successfull reloading
Private
TestClass: TTestClass;
Str: TStream;
Public
Constructor Create(AOwner: TComponent); Override;
Destructor Destroy; Override;
End;
Var
Form1: TForm1;
Implementation
{$R *.dfm}
Procedure TTestClass.SetFa(Const Value: Integer);
Begin
Fa := Value;
ShowMessage('ok for "simple types"....');
End;
Procedure TTestClass.SetFb(Const Value: Int64);
Begin
Fb := Value;
ShowMessage('and for the others');
End;
Procedure TTestClass.SaveInstance(Var Str: TStream);
Begin
Str.Position := 0;
Str.WriteComponent( Self );
End;
Procedure TTestClass.Assign(Source: TPersistent);
Begin
If Not (Source Is TTestClass) Then Inherited
Else
Begin
b := TTestClass(Source).Fb;
End;
End;
Procedure TTestClass.LoadInstance(Var Str: TStream);
Begin
Str.Position := 0;
// this will work for fa and not fb.
Str.ReadComponent(Self);
End;
Procedure TTestClass.ReallyLoadInstance(Var Str: TStream);
Begin
Str.Position := 0;
Assign( Str.ReadComponent(Nil));
End;
Constructor TForm1.Create(AOwner: TComponent);
Begin
RegisterClasses([TTestClass]);
Inherited;
TestClass := TTestClass.Create(Self);
Str := TmemoryStream.Create;
End;
Destructor TForm1.Destroy;
Begin
Str.Free;
Inherited;
End;
Procedure TForm1.Button1Click(Sender: TObject);
Begin
Str.Size := 0;
TestClass.SaveInstance(Str);
End;
Procedure TForm1.Button2Click(Sender: TObject);
Begin
If Str.Size = 0 Then Exit;
TestClass.LoadInstance(Str);
// guess what...only first message
End;
Procedure TForm1.Button3Click(Sender: TObject);
Begin
If Str.Size = 0 Then Exit;
TestClass.ReallyLoadInstance(Str);
End;
As in TypInfo.pas there is a 'tkInt64' case (which seems to call a "SetProc" procedure), Shouldn't published-Int64-props be set using the "Writer" ( as done usually with other "common" types) ?
That's because you never assign a value to property b. Thus it has the default value (zero) and the streaming system won't save it to the stream. And since it isn't in the stream, you won't see the setter called when reading it back...
Actually, since you don't assign value to property a either, same thing should happen with it. Looks like a bug (or at least inconsistency) in the streaming system:
either it shouldn't save/load the Integer property with zero value to the stream too,
or it should save/load both of them as there is no default specifier in the properties definition and thus nodefault should be assumed and thus the value always to be streamed.
So, to recap: add TestClass.b := 1; before calling TestClass.SaveInstance(Str); and you should see the setter called when loading the object back from stream, but you can't relay on the streaming system to call the setter when property has the default value of the type.
This seems to be a bug with Int64 as a property.
As a workaround you could either use another data type, like Integer, or, if that is not big enough, use DefineProperties and TFiler.DefineProperty, TFiler.DefineBinaryProperty, etc.

Frames and Browse History in Delphi

I am currently developing a delphi application that will need a browse history and am trying to work out how exactly to implement this.
The application has 2 modes. Browse and Details. Both designed as Frames.
After a search an appropriate number of Browse Frames are created in Panel 1 and populated.
From a Browse Frame we can either open the Detail Frame, replacing the contents of Panel 1 with the contents of the Detail Frame. Alternatively a new search can be spawned, replacing the current set of results with a new set.
From the Detail Frame we can either edit details, or spawn new searches. Certain searches are only available from the Detail Frame. Others from either the Browse Frames or the Detail Frame.
Each time a user displays the Detail Frame, or spawns a new search I want to record that action and be able to repeat it. Other actions like edits or "more details" won't be recorded. (Obviously if a user goes back a few steps then heads down a different search path this will start the history fresh from this point)
In my mind I want to record the procedure calls that were made in a list e.g.
SearchByName(Search.Text);
SearchByName(ArchName.Text);
DisplayDetails(JobID);
SearchByName(EngineerName.Text);
DisplayDetails(JobID);
Then I can just (somehow) call each item in order as I go bak and forward...
In response to Dan Kelly's request to store the function:
However what I still can't see is how I call the stored function -
What you are referring to is storing a method handler. The code below demonstrates this. But, as you indicated your self, you could do a big if..then or case statement.
This all will works. But an even more "eloquent" way of doing all this is to store object pointers. For example, if a search opens another search, you pass a pointer of the first to the 2nd. Then in the 2nd if you want to refer back to it, you have a pointer to it (first check that it is not nil/free). This is a much more object oriented approach and would lend itself better to situations where someone might close one of the frames out of sequence.
unit searchit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TSearchObject = class
FSearchValue: String;
FOnEventClick: TNotifyEvent;
constructor Create(mSearchValue: string; mOnEventClick: TNotifyEvent);
procedure FireItsEvent;
end;
type
TForm1 = class(TForm)
SearchByName: TButton;
GoBack: TButton;
DisplayDetails: TButton;
searchfield: TEdit;
jobid: TEdit;
procedure FormCreate(Sender: TObject);
procedure SearchByNameClick(Sender: TObject);
procedure GoBackClick(Sender: TObject);
procedure DisplayDetailsClick(Sender: TObject);
private
{ Private declarations }
SearchObjectsList: TStringList;
procedure DisplayDetailFunction(Sender: TObject);
procedure SearchByNameFunction(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
constructor TSearchObject.Create(mSearchValue: string;mOnEventClick: TNotifyEvent);
begin
FOnEventClick := mOnEventClick;
FSearchValue := mSearchValue;
end;
{$R *.dfm}
procedure TSearchObject.FireItsEvent;
begin
if Assigned(FOnEventClick) then
FOnEventClick(self);
end;
procedure TForm1.SearchByNameClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create(SearchField.Text,SearchByNameFunction);
SearchObjectsList.AddObject(SearchField.Text,mSearchObject);
end;
procedure TForm1.DisplayDetailFunction(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject(Sender);
ShowMessage('This is the Display Detail Event. The value of the JobID is '+mSearchObject.FSearchValue);
end;
procedure TForm1.SearchByNameFunction(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject(Sender);
ShowMessage('This is the SearchByName Event. The value of the Search Field is '+mSearchObject.FSearchValue);
end;
procedure TForm1.DisplayDetailsClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create(jobid.text,DisplayDetailFunction);
SearchObjectsList.AddObject(jobid.text,mSearchObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SearchObjectsList := TStringList.Create;
end;
procedure TForm1.GoBackClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
if SearchObjectsList.count=0 then
showmessage('Cannot go Back!')
else begin
mSearchObject := TSearchObject(SearchObjectsList.Objects[SearchObjectsList.count-1]);
mSearchObject.FireItsEvent;
SearchObjectsList.Delete(SearchObjectsList.count-1);
end;
end;
end.
Keep track of everything in a TStringList; when they go "Back" you delete from the string list. This is a sort of prototype:
type
TSearchObject = class
FSearchFunction,FSearchValue: String;
constructor Create(mSearchFunction,mSearchValue: string);
end;
type
TForm1 = class(TForm)
SearchByName: TButton;
GoBack: TButton;
DisplayDetails: TButton;
searchfield: TEdit;
procedure FormCreate(Sender: TObject);
procedure SearchByNameClick(Sender: TObject);
procedure GoBackClick(Sender: TObject);
procedure DisplayDetailsClick(Sender: TObject);
private
{ Private declarations }
SearchObjectsList: TStringList;
jobid: String; //not sure how you get this
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
constructor TSearchObject.Create(mSearchFunction,mSearchValue: string);
begin
FSearchFunction := mSearchFunction;
FSearchValue := mSearchValue;
end;
{$R *.dfm}
procedure TForm1.SearchByNameClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create('SearchByName',SearchField.Text);
SearchObjectsList.AddObject(SearchField.Text,mSearchObject);
end;
procedure TForm1.DisplayDetailsClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create('DisplayDetails',JobID);
SearchObjectsList.AddObject(JobId,mSearchObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SearchObjectsList := TStringList.Create;
end;
procedure TForm1.GoBackClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
if SearchObjectsList.count=0 then
showmessage('Cannot go Back!')
else begin
mSearchObject := TSearchObject(SearchObjectsList.Objects[SearchObjectsList.count-1]);
if mSearchObject.FSearchFunction ='SearchByName' then
ShowMessage('Value of Search Field:'+mSearchObject.FSearchValue)
else
ShowMessage('Value of JobID:'+mSearchObject.FSearchValue);
SearchObjectsList.Delete(SearchObjectsList.count-1);
end;
end;
Another option would be to use my wizard framework, which does this with TForms but can easily also be adjusted to use frames. The concept is that each summary form knows how to create its appropriate details. In your case the framework is more of an example of how to do it, rather than a plug and play solution.
Complementing MSchenkel answer.
To persist the list between program runs, use an ini file.
Here is the idea. You have to adapt it. Specially, you have to figure out the way to convert object to string and string to object, sketched here as ObjectToString(), StringToStringID and StringToObject().
At OnClose event, write the list out to the ini file.
const
IniFileName = 'MYPROG.INI';
MaxPersistedObjects = 10;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
ini: TIniFile;
i: integer;
cnt: integer;
begin
ini:=TIniFile.Create(iniFileName);
cnt:=SearchObjectsList.Count;
if cnt>MaxPersistedObjects then
cnt:=MaxPersistedObjects;
for i:=1 to MaxPersistedObjects do
if i>cnt then
ini.WriteString('SearchObjects','SearchObject'+intToStr(i),'');
else
ini.WriteString('SearchObjects','SearchObject'+intToStr(i),
ObjectToString(SearchObjectsList[i-1],SearchObjectsList.Objects[i-1]) );
ini.Free;
end;
and read it back at OnCreate event.
procedure TForm1.FormCreate(Sender: TObject);
var
ini: TIniFile;
i: integer;
begin
SearchObjectsList := TStringList.Create;
ini:=TIniFile.Create(IniFileName);
for i:=1 to MaxPersistedObjects do
begin
s:=ini.ReadString('SearchObjects','SearchObject'+intToStr(i),'');
if s<>'' then
SearchObjectsList.AddObject(StringToID(s),StringToObject(s));
end;
ini.Free;
end;

Resources