How do you log a specific table in Accuracer? - delphi

Code like this logs all table inserts (from the entire application):
procedure TForm1.ACRDatabase1AfterInsertRecord(Sender: TACRDataSet;
const TableName: WideString; const FieldValues: TACRArrayOfTACRVariant);
begin
if (AnsiUpperCase(TableName) = AnsiUpperCase(LogTable.TableName)) then
Exit;
if (Sender is TACRTable) then
LogTable.Insert();
LogTable.FieldByName('EventTime').AsDateTime := Now;
LogTable.FieldByName('TableName').AsString := TableName;
LogTable.FieldByName('EventType').AsString := 'Insert ';
LogTable.FieldByName('Whatever').AsString := FieldValues[4].AsString;
LogTable.Post();
end;
But fieldValues are different for each table so you might crash
the application (almost sure) using fieldvalues i.e their index number.
How do you overcome this ? Is it possible to log each table separately ?

As I mentioned in a comment, I don't have Accuracer, but thought it might be helpful
to post a generic method of doing client-side logging, which can capture the value
of one or more fields and be used for as many datasets as you need. You may be
able to use part of it in your ACRDatabase1AfterInsertRecord handler, as its Sender
parameter appears to identify the dataset into which the new row has been inserted.
As you can see, there is a LogFields procedure which can be included in the AfterInsert
handler of any dataset you like and this calls a separate GetFieldsToLog procedure which
adds the names of the field(s) to log for a given dataset to a temporary StringList. It's
only the GetFieldsToLog procedure which needs to be adapted to the needs of a given set of datasets.
procedure TForm1.GetFieldsToLog(ADataSet : TDataSet; FieldList : TStrings);
begin
FieldList.Clear;
if ADataSet = AdoQuery1 then begin
FieldList.Add(ADataSet.Fields[0].FieldName);
end
else
// obviously, deal with other specific tables here
end;
procedure TForm1.LogFields(ADataSet : TDataSet);
var
TL : TStringList;
i : Integer;
ValueToLog : String;
begin
TL := TStringList.Create;
try
GetFieldsToLog(ADataSet, TL);
for i := 0 to TL.Count - 1 do begin
ValueToLog := ADataSet.FieldByName(TL[i]).AsString;
// do your logging here however you want
end;
finally
TL.Free;
end;
end;
procedure TForm1.ADOQuery1AfterInsert(DataSet: TDataSet);
begin
LogFields(DataSet);
end;
Btw, one of the points of having a separate GetFieldsToLog procedure is that it helps to extend
client-side logging to changes in existing dataset records. If you generate this list
at start-up and save it somewhere, you can use it in the BeforePost event of a dataset to
pick up the current and previous values of the field (using its Value and OldValue properties),
save those in an another StringList and log them in the AfterPost event. Of course,
if you'e using a common store for these value from more than one dataset, you need to make
sure that the AfterPost of one dataset fire before the BeforePost of any other, or do the logging
entirely within the BeforePost (having to store the old and current field values between
Before- and AfterPost is messy, and it would be better to do everything in the AfterPost,
but unfortunately the OldValue is out-of-date by the time AfterPost occurs.
Be aware that getting the OldValue requires the specific dataset type to correctly implement
it. Not all types of dataset I've come across do, though, so it needs checking.
Btw #2, supposing you have a procedure like this
procedure TForm1.DoSomething(AnObject : TObject);
then you can use "if AnObject is ..." to do something like this
var
AnAdoQuery : TAdoQuery;
begin
if AnObject is TAdoQuery then begin
// First, use a cast to assign Sender to the local AnAdoQuery variable
AnAdoQuery := TAdoQuery(AnObject);
// Then, we can do whatever we like with it, e.g.
Caption := AnAdoQuery.Name;
end;
end;
Otoh, if for some reason (and I can't immediately think why we would want to but never mind)
we just want to check that what we've been passed as the AnObject parameter is a particular
object, we can omit the cast and just do
if AnObject = AdoQuery1 then
ShowMessage('Received AdoQuery1');
This equality check works, regardless of the actual class of what we've been passed
as the AnObject parameter because all other classes are descendants of AnObject's
declared class, namely TObject.

Related

How to pass DataSet: TDataSet as procedure parameter

I am setting up a new procedure which will show a message after executing a query. I am using the "AfterOpen" Event where i have to pass the "DataSet: TDataSet" parameter.
procedure Tf_SeznamDluzniku.ShowInfoMessage(DataSet: TDataSet; info : string);
begin
l_InfoMessage.Caption := info;
img_success.Visible := True;
end;
query.AfterOpen := ShowInfoMessage(,'InfoMessage here')
Can somebody please explain me what is the DataSet variable and what i have to pass to the procedure as the first parameter ?
If it is attached to the event, it is the dataset that triggered the AfterOpen event. The dataset itself will call the procedure, and pass itself in that parameter.
But you added the Info parameter, which makes the procedure invalid as an event handler. Where do you want that info to come from? From the dataset?
Since it's an event handler, it's bad practise to call it yourself. You could do it, and just pass nil (or a specific dataset), since it's not used anyway. But you can get into weird situations, because it looks like the method is only going to be called after open, but then it turns out it's called on other occasions as well.
So it's better to make a separate procedure to do what you want, and call that from the AfterOpen event handler. You can pass in info from the dataset, but you can also call that procedure from somewhere else, for instance to provide some initial caption until the dataset is opened:
// The procedure doesn't need the dataset, only the info to set.
procedure Tf_SeznamDluzniku.ShowInfoMessage(Info : string);
begin
l_InfoMessage.Caption := info;
end;
// The actual event handler for YourDataset.OnAfterOpen (you have to attach them)
// This can set the info, and/or maybe set the success indicator right away..
procedure Tf_SeznamDluzniku.YourDataSetAfterOpen(DataSet: TDataSet);
begin
ShowInfoMessage(DataSet.FieldByName('info').AsString);
img_success.Visible := True;
end;
// For demonstration, another event handler for showing the form, to put in some initial caption.
procedure Tf_SeznamDluzniku.FormShow(const Sender: TObject);
begin
ShowInfoMessage('Loading...');
end;

Is it possible to sort a TListBox using a custom sort comparator?

I need to sort my TListBox but I realized it is a lot of work to modify my code if I were to say make a TStringList, sort it and then copy those items into the Listbox. The main reason it's a lot of work is that I have many places in the code where the listbox contents are modified and I guess I would have to edit them all to force a sort at the time they are added, deleted or whatever.
I would much prefer something that let me just attach a method to a listbox somehow to sort it using my custom sort logic.
Is it somehow possible?
This is no Problem! Look at this Code:
function CompareDates(List: TStringList; Index1, Index2: Integer): Integer;
var
d1, d2: TDateTime;
begin
d1 := StrToDate(List[Index1]);
d2 := StrToDate(List[Index2]);
if d1 < d2 then
Result := -1
else if d1 > d2 then
Result := 1
else
Result := 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
sl: TStringList;
begin
sl := TStringList.Create;
try
sl.Assign(ListBox1.Items);
sl.CustomSort(CompareDates);
ListBox1.Items.Assign(sl);
finally
sl.Free
end;
end;
If you are using Delphi XE or later, I have a possibility for you.
Note that I say "possibility" and not "solution" as it is more of a hack than anything else and I wouldn't really approve this in production code unless it was a last resort.
From what I understand, what you are essentially trying to achieve is override the behavior of the Add function (which is virtual) to make it insert at the right position based on a custom order. (If you need to also override insert, this works too). If it was possible to override the TStrings descendant TListbox uses, that would be simple, but we are not that lucky.
Delphi XE introduced a new class called TVirtualMethodInterceptor (Rtti unit) that allows to intercept virtual method to do whatever we want to do with it. We can inspect and modify the parameters, call other functions, or litterally cancel the call and do nothing at all.
Here's how the proof of concept I made looked like:
//type
// TCompareFunc<T1> = reference to function (const Arg1, Arg2 : T1) : Integer;
procedure TForm4.FormCreate(Sender: TObject);
var vCompareFunc : TCompareFunc<string>;
RttiContext : TRttiContext;
vAddMethod : TRttiMethod;
vRttiType : TRttiType;
begin
RttiContext := TRttiContext.Create;
vRttiType := RttiContext.GetType(TStrings);
vAddMethod := vRttiType.GetMethod('Add');
vCompareFunc := MyCompareFunc;
Fvmi := TVirtualMethodInterceptor.Create(Listbox1.Items.ClassType);
Fvmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue)
var
idx : Integer;
begin
if Method = vAddMethod then
begin //if it's the Add method, map it to Insert at the right position
DoInvoke := False;
BinarySearch(TStrings(Instance), Args[0].AsString, vCompareFunc,idx);
TStrings(Instance).Insert(idx, Args[0].AsString);
end;
end;
Fvmi.Proxify(Listbox1.Items);
end;
This proof of concept intercept the call to TStrings.add and map it to binarysearch/Insert so that the items of the list are always in the right order. This does not override the Insert or Assign function, or any other function modifying the list. If you want to use this approach, you need to override all the "offending" functions.
Disclaimer : Since I have never really used this technique, don't consider this example as the golden rule for TVirtualMethodInterceptor's usage. It does work, but it might have performance implications or others that I'm unaware of.
One important point to mention (from Barry Kelly's blog, see below)
One thing the TVirtualMethodInterceptor class doesn't have, however,
is a way to unhook (unproxify) the object. If the object is never
unhooked, it's important that the object doesn't outlive the
interceptor, because the interceptor needs to allocate executable
memory in order to create the little stubs with which it redirects
method calls to the events.
If you want to dig deeper, here's a pretty good article on the subject:
http://blog.barrkel.com/2010/09/virtual-method-interception.html

Transfer statusbar values from one form to another

I have two identical statusbars (AdvOfficeStatusBar) on each form. That means Form1 has the same status bar as the Form2.Now,before I close the Form1 I would like all the values from the status bar to be transfered to that one on the form2. I suppose I could do it one by one like... :
procedure TForm2.FormShow(Sender: TObject);
begin
AdvOfficeStatusBar1.Panels[0].Text := Form1.AdvOfficeStatusBar1.Panels[0].Text;
AdvOfficeStatusBar1.Panels[1].Text := Form1.AdvOfficeStatusBar1.Panels[1].Text;
AdvOfficeStatusBar1.Panels[2].Text := Form1.AdvOfficeStatusBar1.Panels[2].Text;
AdvOfficeStatusBar1.Panels[4].Text := Form1.AdvOfficeStatusBar1.Panels[4].Text;
AdvOfficeStatusBar1.Panels[5].Text := Form1.AdvOfficeStatusBar1.Panels[5].Text;
AdvOfficeStatusBar1.Panels[6].Text := Form1.AdvOfficeStatusBar1.Panels[6].Text;
end;
I was wondering if there's a more simple way?Less code...
You're suffering from an anti-pattern called copy-paste-programming.
It makes for very easy programming, but difficult maintenance.
Every time you add a line to one statusbar, you have to go back and update to code to have it be linked into the other statusbar.
It's easy to forget updating the code and ehm well it's work, which is why this is bad practice.
A better way is to use Assign or if that does not work a loop. Both are demonstrated below.
Note that the Panel is an array property.
Normally every array_property has a associated count property.
I'm not sure what it is in this instance, but I'm guessing it's called PanelCount.
As per David's suggestion it's better to store the state somewhere inside your program, because you might redesign the form and lose the StatusBar, in which case you'd also lose the storage.
type
TForm2 = class(TForm)
private
StatusStore: array of string;
.....
end;
implementation
procedure TForm2.FormCreate(Sender: TObject);
begin
//Initialisation, you cannot use a loop, unless you'd read it from a file.
SetLength(StatusStore,6);
StatusStore[0]:= 'a';
StatusStore[1]:= 'b';
StatusStore[2]:= 'c';
StatusStore[3]:= 'd';
StatusStore[4]:= 'e';
StatusStore[5]:= 'f';
end;
procedure TForm2.FormShow(Sender: TObject);
var
i,maxi: integer;
begin
StatusStore[0]:= 'Showing Form2';
Maxi:= SizeOf(StatusStore);
i:= 0;
AdvOfficeStatusBar1.PanelCount:= Maxi;
while (i < Maxi) do begin
AdvOfficeStatusBar1.Panels[i].Text:= StatusStore[i];
end; {while}
Form1.AdvOfficeStatusBar1.Panels.Assign(Form2.AdvOfficeStatusBar1.Panels);
end;
Now whatever data is to be displayed and however many items there are, the display will update.
You can even program the loop to skip an item if you want the first or last item to be different for each form.

Storing extra data in developer express component cxTreeView

I have a cxTreeView on a form displaying descriptions of some files. I would like to be able to have the option of clicking one of the descriptions and have the filename returned.
In order to do this I need to store some extra data in the treeview and that is my problem.
How do I do this:
Store both filename and description, display just the description but get the filename by doubleclicking?
I think you can use a TcxTreeList for this task (if possible), if there is no urgent need to use a TcxTreeView. In a TcxTreeList you can add a new column and store the filename in it. Then make this column invisible.
If you must rely on a TcxTreeView, there is no option to directly store data, like strings, in the TTreeNodes. You can store a pointer to any object into the property "Data" of the TTreeNode.
In both answers here is mentioned that TcxTreeView uses standard TTreeNode where, as Warren pointed in his answer as first, you can use the TTreeNode.Data property to store your data. I'll just complete these thoughts with an example.
Update:
The previous version of this post would work only for constant strings for whose the space is being allocated automatically. If you need to pass a variable into the TTreeNode.Data you have to allocate and release the memory by your own. Here is the example with using simple helper functions.
The AddFileNode helper function adds the node to the Items into the ParentNode with the name of the FileDesc and allocates the space and copy the passed FileName for the TTreeNode.Data by using the StrNew function.
When you need to modify the data, you should release (or better, but little bit complicated would be to reallocate) the allocated memory and allocate the space and copy the new value. For this you can use the ChangeFileName which disposes the string memory allocated before and allocates the space and copy the passed FileName value.
As I mentioned before you need to take care of the memory disposal by your own and for this there is the OnDeletion event the best place. So write the handler for this event where you'll release the memory allocated when adding the items.
function AddFileNode(Items: TTreeNodes; ParentNode: TTreeNode;
const FileName, FileDesc: string): TTreeNode;
begin
Result := Items.AddChildObject(ParentNode, FileDesc, StrNew(PChar(FileName)));
end;
function ChangeFileName(TreeNode: TTreeNode; const FileName: string): Boolean;
begin
Result := False;
if Assigned(TreeNode.Data) then
begin
Result := True;
StrDispose(PChar(TreeNode.Data));
TreeNode.Data := StrNew(PChar(FileName));
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
S: string;
begin
S := 'C:\FileName 1.xxx';
AddFileNode(cxTreeView1.Items, nil, S, 'File 1');
S := 'C:\FileName 2.xxx';
AddFileNode(cxTreeView1.Items, nil, S, 'File 2');
end;
procedure TForm1.cxTreeView1Deletion(Sender: TObject; Node: TTreeNode);
begin
if Assigned(Node.Data) then
StrDispose(PChar(Node.Data));
end;
procedure TForm1.cxTreeView1DblClick(Sender: TObject);
var
CurrentPos: TPoint;
CurrentNode: TTreeNode;
begin
CurrentPos := cxTreeView1.ScreenToClient(Mouse.CursorPos);
if (htOnItem in cxTreeView1.GetHitTestInfoAt(CurrentPos.X, CurrentPos.Y)) then
begin
CurrentNode := cxTreeView1.GetNodeAt(CurrentPos.X, CurrentPos.Y);
if Assigned(CurrentNode) and Assigned(CurrentNode.Data) then
ShowMessage(PChar(CurrentNode.Data));
end;
end;
If the nodes are regular TTreeNode, you can use the Data:Pointer value in the Node object.
If the nodes in your tree are of type TcxTreeListNode, they contain the ability to store any data values you want. WHen I look at the sources, I see these properties in the node-level:
property ValueCount: Integer read GetValueCount;
property Values[Index: Integer]: Variant read GetValue write SetValue;

Delphi & shared datasources

In my app I have different forms that use the same datasource (so the queries are the same too), defined in a common datamodule. Question is, is there a way to know how many times did I open a specific query? By being able to do this, I could avoid close that query without closing it "every where else".
Edit: It's important to mention that I'm using Delphi3 and it is not a single query but several.
The idea is to use the DataLinks property of the TDataSource.
But, as it is protected, you have to gain access to it. One common trick is to create a fake descendant just for the purpose of casting:
type
TDataSourceHack = class(TDataSource);
Then you use it like:
IsUsed := TDataSourceHack(DataSource1).DataLinks.Count > 0;
You can get creative using a addref/release like approach. Just create a few functions and an integer variable in your shared datamodule to do the magic, and be sure to call them..partial code follows:
TDMShared = class(tDataModule)
private
fQueryCount : integer; // set to 0 in constructor
public
function GetQuery : tDataset;
procedure CloseQuery;
end;
function TDMShared.GetQuery : tDataset;
begin
inc(fQueryCount);
if fQueryCount = 1 then
SharedDatsetQry.open;
Result := shareddatasetqry; // your shared dataset here
end;
procedure TDMShared.CloseQuery;
begin
dec(fQueryCount);
if fQueryCount <= 0 then
shareddatasetqry.close; // close only when no refs left.
end;
EDIT: To do this with multiple queries, you need a container to hold the query references, and a way to manipulate them. a tList works well for this. You will need to make appropriate changes for your TDataset descendant, as well as create a FreeAndNil function if you are using an older version of Delphi. The concept I used for this was to maintain a list of all queries you request and manipulate them by the handle which is in effect the index of the query in the list. The method FreeUnusedQueries is there to free any objects which no longer have a reference...this can also be done as part of the close query method, but I separated it to handle the cases where a specific query would need to be reopened by another module.
Procedure TDMShared.DataModuleCreate(Sender:tObject);
begin
dsList := tList.create;
end;
Function TDMShared.CreateQuery(aSql:String):integer;
var
ds : tAdoDataset;
begin
// create your dataset here, for this example using TADODataset
ds := tAdoDataset.create(nil); // self managed
ds.connection := database;
ds.commandtext := aSql;
ds.tag := 0;
Result := dsList.add(ds);
end;
function TDMShared.GetQuery( handle : integer ) : tDataset;
begin
result := nil;
if handle > dsList.count-1 then exit;
if dsList.Items[ handle ] = nil then exit; // handle already closed
result := tAdoDataset( dsList.items[ handle ]);
Inc(Result.tag);
if Result.Tag = 1 then
Result.Open;
end;
procedure TDMShared.CloseQuery( handle : integer );
var
ds : tAdoDataset;
begin
if handle > dsLIst.count-1 then exit;
ds := tAdoDataset( dsList.items[ handle ]);
dec(ds.Tag);
if ds.Tag <= 0 then
ds.close;
end;
procedure TDMShared.FreeUnusedQueries;
var
ds : tAdoDataset;
ix : integer;
begin
for ix := 0 to dsList.Count - 1 do
begin
ds := tAdoDataset(dsLIst.Items[ ix ]);
if ds.tag <= 0 then
FreeAndNil(dsList.Items[ix]);
end;
end;
procedure TDMShared.DataModuleDestroy(Sender: TObject);
var
ix : integer;
begin
for ix := 0 to dsList.count-1 do
begin
if dsLIst.Items[ix] <> nil then
FreeAndNil(dsLIst.Items[ix]);
end;
dsList.free;
end;
Ok, a completely different solution...one that should work for Delphi 3.
Create a new "Descendant Object" from your existing dataset into a new unit, and add some behavior in the new object. Unfortunately I do not have Delphi 3 available for testing, but it should work if you can find the proper access points. For example:
TMySharedDataset = class(tOriginalDataset)
private
fOpenCount : integer;
protected
procedure Internal_Open; override;
procedure Internal_Close; override;
end;
TMySharedDataset.Internal_Open;
begin
inherited Internal_Open;
inc(fOpenCount);
end;
TMySharedDataset.Internal_Close;
begin
dec(fOpenCount);
if fOpenCount <= 0 then
Inherited Internal_Close;
end;
Then just include the unit in your data module, and change the reference to your shared dataset (you will also have to register this one and add it to the palette if your using components). Once this is done, you won't have to make changes to the other units as the dataset is still a descendant of your original one. What makes this all work is the creation of YOUR overridden object.
You could have a generic TDataSet on the shared datamodule and set it on the OnDataChange, using the DataSet property of the Field parameter
dstDataSet := Field.DataSet;
This way, when you want to close the dataset, close the dataset on the datamodule, which is a pointer to the correct DataSet on some form you don't even have to know

Resources