(Based on comments I have edited the question to focus on the actual code part causing the issue, and added a section at the end)
This is the local variables window in my Delphi 10.3.4 Win32 app, note that lData is missing:
The breakpoint corresponding to that screenshot is on the if EmptyJSONArray(lData) line at the end of the next code snippet:
function TLoketAPI.GetOneBatch(AJSONToXML: TJSONToXML; const ASelProps: TSelectionProps; var ABatchCount, AStatusCode: Integer): Boolean;
var
lURL,
lNextID,
lData : String;
lLogHTTP : TLogHTTP;
begin
Result := false;
if FAbort then Exit;
lLogHTTP := TLogHTTP.Create(nil);
Result := false;
if ABatchCount = 0 then
lNextID := ASelProps.GetNextIDValue
else
lNextID := ASelProps.GetSameIDValue;
lURL := StringReplace(ASelProps.URL,'{id}',lNextID,[rfReplaceAll,rfIgnoreCase]);
if ABatchCount <> 0 then
if Pos('?',lURL) = 0 then lURL := lURL + '?pageNumber=' + IntToStr(ABatchCount+1)
else lURL := lURL + '&pageNumber=' + IntToStr(ABatchCount+1);
try
SetHTTPJSONProperties(lLogHTTP);
try
lLogHTTP.Get(lURL);
if FAbort then
Exit;
lData := lLogHTTP.TransferredData;
if FSaveJSON then
begin
lBatchCount := ABatchCount;
TThread.Synchronize(nil,
procedure
begin
if lNextID = '' then
SaveToJSONFile(Format('%s batch (%d)',[ASelProps.TextID,lBatchCount]),lData)
else
SaveToJSONFile(Format('%s (%s) batch (%d)',[ASelProps.TextID,lNextID,lBatchCount]),lData);
end);
end;
if EmptyJSONArray(lData) then
lData contains valid data returned from the logHTTP call.
At the breakpoint (or anywhere else in the routine), lData does not show up in the local variables and cannot be inspected or watched.
The IDE gives:
E2003 Undeclared identifier: 'lData'
This stuff is running in the main thread, optimization is off.
If I comment out the TThread.Synchronize, lData can again be inspected/watched.
What is going on here?
(Added)
In the comments, Remy and Dalija suggested that anonymous procedure as the reason, and linked this to the Embarcadero issue RSP-22924 Watch shows undeclared identifier for captured local variables (also reported under RSP-21917).
I still don't get it though: RSP-22924 was edited to change its type to Feature. Feature? It looks like a bug to me.
And: (How) can this behavior be prevented? I'd really like to inspect lData.
Related
I have one installation that behaves VERY oddly... Every time we try to copy something on a network drive we check accessibility with code like this:
procedure TForm1.TestAccess;
var fn : string;
hdl : THandle;
res : boolean;
dir : string;
flags : Cardinal;
begin
dir := edDir.Text;
flags := FILE_FLAG_DELETE_ON_CLOSE or FILE_FLAG_NO_BUFFERING or FILE_ATTRIBUTE_HIDDEN;
fn := FindUnusedFileName( IncludeTrailingPathDelimiter( dir ) + IntToStr( Random(10000) ) + '.tst' );
memLog.Lines.Add('Try to create file: ' + fn);
hdl := CreateFile( PChar(fN), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_DELETE, nil, CREATE_NEW,
flags, 0 );
res := hdl <> INVALID_HANDLE_VALUE;
if not res then
begin
memLog.Lines.Add('Error: ' + SysErrorMessage(GetLastError));
end
else
memLog.Lines.Add('Success');
if res then
CloseHandle(hdl);
end;
where memLog is a TMemo and edDir is simply an edit field.
Now here is the strange part... I get an access denied on that system meaning uploading will fail most of the time (most of the time is the strange part here).
Another thing is that in a first attempt I used the JvDirectoryEdit control. In that case the result is twofold... If I enter the directory (UNC Path) there without a backslash I get the access denied too. If I enter a final backslash and the combo box window pops up showing the content (aka directories) in that folder it finally works!!!
So... First: has anyone a clue what the problem might be and do I something wrong here?
I have used this procedure in the past to check if a directory is read-only, maybe it can be useful:
FUNCTION DirRO(NomeCartella : String) : Boolean;
VAR VarFile : TextFile;
NomCart : String;
BEGIN
Result := False;
If NomeCartella[Length(NomeCartella)]='\'
Then NomCart := NomeCartella
Else NomCart := NomeCartella+'\';
Try AssignFile(VarFile,NomCart+'^ghi.kol');
{$I-}
Rewrite(VarFile);
{$I-}
If IOResult<>0 Then
Begin
Result := True;
Exit;
End;
CloseFile(VarFile);
Erase(VarFile);
Except Result := True;
End;
END;
I am in the process of upgrading code from Delphi 10 Seattle to Delphi 10.2 Tokyo and get a lot of H2077 hints Value assigned to ... never used on assignments.
(Even in places where these were explicitly added in the past to get rid of 'may not have a value' warnings).
These are all function initialized like:
Result := 0;
...
Or:
Result := ftType1; // where ftType1 is an enumerated type
...
Did the compiler get smarter in detecting these or has something changed regarding the initial return values of functions?
We have always had these hints 'on', and I always build (not compile).
Example function (1) that builds without hints in Seattle,
but gives the hint H2077 Value assigned to 'GetDatabaseDialect' not used on the first Result := 0 line in Tokyo.
function GetDatabaseDialect(DBName, User, Pswd: string) : integer;
var
status: array[1..19] of longint;
szDbName, szDbParam: PANSIChar;
dbHandle : pointer;
rslt: longint;
lDPBBuffer : ANSIString;
lDPBLength : integer;
cItem: ANSIChar;
szRslt: PANSIChar; //array[0..IBResultBufferSize-1] of ANSIChar;
begin
Result := 0;
dbHandle := nil;
// init database parameter block with version number
lDPBBuffer := '';
SetLength(lDPBBuffer, 1);
lDPBBuffer[1] := ANSIChar(isc_dpb_version1);
lDPBLength := 1;
// fill Database Parameter Buffer with user name/password
lDPBBuffer := lDPBBuffer +
ANSIChar(isc_dpb_user_name) +
ANSIChar(Length(User)) +
ANSIString( User );
Inc(lDPBLength, 2 + Length(User));
lDPBBuffer := lDPBBuffer +
ANSIChar(isc_dpb_password) +
ANSIChar(Length(Pswd)) +
ANSIString( Pswd );
Inc(lDPBLength, 2 + Length(Pswd));
//Pointers naar naam + buffer
szDbName := PANSIChar(ANSISTring(DBName));
szDbParam := PANSIChar( lDPBBuffer );
// attach to the database and set dialect
rslt := isc_attach_database(#status, 0, szDbName, #dbHandle, lDPBLength, szDbParam);
if rslt <> 0 then
raise EDatabaseError.Create('Error attaching database! ISC# ' + IntToStr(rslt));
//Haal sql dialect op
szRslt := AllocMem(1000);
try
FillChar( szRslt^, 1000, 0);
cItem := ANSIChar( isc_info_db_SQL_dialect );
rslt := isc_database_info(#status, #DBHandle, 1, #cItem, 1000, szRslt);
if rslt <> 0 then
raise EDatabaseError.Create('Error retrieving database info ! ISC# ' + IntToStr(rslt));
Result := Ord(szRslt[3]); //3e positie is dialect
finally
FreeMem(szRslt);
end;
// Drop the connection to the database
rslt := isc_detach_database(#status, #dbHandle);
if rslt <> 0 then
raise EDatabaseError.Create('Error detaching database! ISC# ' + IntToStr(rslt));
end;
Example (2) from a third party library that does not seem to be optimized for Tokyo,
illustrating the case with enumerated types:
H2077 Value assigned to 'TppTemplate.StreamType' not used
Note that changing the assignment to Result := ftASCII; does not make the hint go away (my initial assumption that it was associated with the first enumeration value was incorrect).
type TppFormatType = (ftBinary, ftASCII);
function TppTemplate.StreamType(aStream: TStream): TppFormatType;
var
lSavePos: Integer;
begin
{save stream position}
lSavePos := aStream.Position;
Result := ftBinary;
try
ComputeOffsetFromStream(aStream);
aStream.Seek(FOffset, soBeginning);
if IsValidASCIISignature(aStream) then
Result := ftASCII
else if IsValidBinarySignature(aStream) then
Result := ftBinary
else
raise EInvalidTemplateError.Create(ppLoadStr(49));
finally
{restore stream position}
aStream.Seek(lSavePos, soBeginning);
end;
end; {function, StreamType}
The common denominator seems to be the Result assignments being in try/finally blocks.
Consider this code with a minimal reproduction of your scenario:
function Bar: Boolean;
begin
Result := Random<0.5;
end;
function Foo: Integer;
begin
Result := 0;
if Bar then
Result := 1
else
raise Exception.Create('');
end;
The compiler, even older versions, emits the following hint:
[dcc32 Hint]: H2077 Value assigned to 'Foo' never used
This is reasonable. The first assignment to Result is pointless and can be removed.
Now consider this variation:
function Foo: Integer;
begin
Result := 0;
try
if Bar then
Result := 1
else
raise Exception.Create('');
finally
end;
end;
Older versions of the compiler no longer emit the hint, but the latest version of the compiler does. This should be considered a compiler defect, for older versions. The two variants of Foo shown above are semantically identical. The compiler would be justified in generating identical code.
As you surmise, the assignment being inside the try/finally block is necessary to trigger the defect in previous versions.
We can conclude that the Embarcadero developers have fixed a defect in Tokyo. You can resolve the hints by removing the spurious initial assignments.
Of course, if your code is to be compiled by older versions of the compiler, as well as by new versions, then you are in a bind. With the code as it stands now, a hint is emitted by new versions of the compiler. Remove the initial assignment and a hint is emitted by old versions of the compiler.
After looking at Delphi extract string between to 2 tags and trying the code given there by Andreas Rejbrand I realized that I needed a version that wouldn't stop after one tag - my goal is to write all the values that occur between two strings in several .xml files to a logfile.
<screen> xyz </screen> blah blah <screen> abc </screen>
-> giving a logfile with
xyz
abc
... and so on.
What I tried was to delete a portion of the text read by the function, so that when the function repeated, it would go to the next instance of the desired string and then write that to the logfile too until there were no matches left - the boolean function would be true and the function could stop - below the slightly modified function as based on the version in the link.
function ExtractText(const Tag, Text: string): string;
var
StartPos1, StartPos2, EndPos: integer;
i: Integer;
mytext : string;
bFinished : bool;
begin
bFinished := false;
mytext := text;
result := '';
while not bFinished do
begin
StartPos1 := Pos('<' + Tag, mytext);
if StartPos1 = 0 then bFinished := true;
EndPos := Pos('</' + Tag + '>', mytext);
StartPos2 := 0;
for i := StartPos1 + length(Tag) + 1 to EndPos do
if mytext[i] = '>' then
begin
StartPos2 := i + 1;
break;
end;
if (StartPos2 > 0) and (EndPos > StartPos2) then
begin
result := result + Copy(mytext, StartPos2, EndPos - StartPos2);
delete (mytext, StartPos1, 1);
end
So I create the form and assign a logfile.
procedure TTagtextextract0r.FormCreate(Sender: TObject);
begin
Edit2.Text:=(TDirectory.GetCurrentDirectory);
AssignFile(LogFile, 'Wordlist.txt');
ReWrite(LogFile);
CloseFile(Logfile);
end;
To then get the files in question, I click a button which then reads them.
procedure TTagtextextract0r.Button3Click(Sender: TObject);
begin
try
sD := TDirectory.GetCurrentDirectory;
Files:= TDirectory.GetFiles(sD, '*.xml');
except
exit
end;
j:=Length(Files);
for k := 0 to j-1 do
begin
Listbox2.Items.Add(Files[k]);
sA:= TFile.ReadAllText(Files[k]);
iL:= Length(sA);
AssignFile(LogFile, 'Wordlist.txt');
Append(LogFile);
WriteLn(LogFile, (ExtractText('screen', sA)));
CloseFile (LogFile);
end;
end;
end.
My problem is that without the boolean loop in the function, the application only writes the one line per file and then stops but with the boolean code the application gets stuck in an infinite loop - but I can't quite see where the loop doesn't end. Is it perhaps that the "WriteLn" command can't then output the result of the function? If it can't, I don't know how to get a new line for every run of the function - what am I doing wrong here?
First you need to get a grip on debugging
Look at this post for a briefing on how to pause and debug a program gone wild.
Also read Setting and modifying breakpoints to learn how to use breakpoints. If you would have stepped through your code, you would soon have seen where you go wrong.
Then to your problem:
In older Delphi versions (up to Delphi XE2) you could use the PosEx() function (as suggested in comments), which would simplify the code in ExtractText() function significantly. From Delphi XE3 the System.Pos() function has been expanded with the same functionality as PosEx(), that is, a third parameter Offset: integer
Since you are on Delphi 10 Seattle you can use interchangeably either System.StrUtils.PosEx() or System.Pos().
System.StrUtils.PosEx
PosEx() returns the index of SubStr in S, beginning the search at
Offset
function PosEx(const SubStr, S: string; Offset: Integer = 1): Integer; inline; overload;
The implementation of ExtractText() could look like this (with PosEx()):
function ExtractText(const tag, text: string): string;
var
startPos, endPos: integer;
begin
result := '';
startPos := 1;
repeat
startPos := PosEx('<'+tag, text, startpos);
if startPos = 0 then exit;
startPos := PosEx('>', text, startPos)+1;
if startPos = 1 then exit;
endPos := PosEx('</'+tag+'>', text, startPos);
if endPos = 0 then exit;
result := result + Copy(text, startPos, endPos - startPos) + sLineBreak;
until false;
end;
I added sLineBreak (in unit System.Types) after each found text, otherwise it should work as you intended it (I believe).
I'm using MySQL, and I know that Nested Connection are not allowed - use "save points" for this - but I would like create a more generic code that could also be used with other DBMS.
So, I would like know how to properly start, working and finish a transaction in the code below?
Once ExampleDAO.Save() function could be used inside other function, like OtherExampleDAO.Save(), I need verify a transaction has been started before I try start a new one.
The lines with the verification if Assigned(dbTransaction) then always returns true, so how to properly verify if dbTransaction was instantiated?
function TExampleDAO.Save(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
Result := False;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
You need to properly initialize dbxTransaction to nil at the start of your function. Local variables in Delphi (on the Win32 platform, at least) are not initialized until a value is assigned to them, meaning that the content is unknown. Passing any value other than nil to Assigned will result in True. I recommend never testing a local variable's content on any platform until it has had a value assigned in your code.
Here's an example of how to make it work. (I've also removed the unnecessary assignment to Result in the exception block.)
function TExampleDAO.Salve(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
dbxTransaction := nil; // Initialize the transaction variable here
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
As was noted by #SirRufo in the comments to your question, failing to pass Example as a parameter should probably raise an exception as well, which would mean that it could become a procedure instead of a function and Result would no longer apply at all.
I have dynamically created TValueListEditor VCL component on a TForm. The code is located in nested procedure of one of the main form's methods. I have set:
ValueListEditor.KeyOptions := [keyEdit, keyAdd, keyUnique];
It looks like this:
TMainForm.Method();
Method has a nested procedure that contains code that creates the components mentioned above.
Then, I have helper function:
function GetMenuListData(XMLNode: TXMLNode; const XNMLDoc: string = '') : string;
In this helper I use this code to load an XML file and then retrieve its nodes and insert them into ValueListEditor.
XMLDoc := TXMLDocument.Create(Self);
XMLDoc.ParseOptions := [poPreserveWhiteSpace];
try
XMLDoc.LoadFromFile(XNMLDoc);
try
Control := FindControl(FindWindow('TForm',PChar('(' + ExtractFileExt(Form1.Edit1.Text) + ')')));
if Control <> nil then
begin
TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1] := XMLDoc.DocumentElement.NodeName;
if XMLDoc.DocumentElement.ChildNodes.First.AttributeNodes.Count > 0 then
TValuelistEditor(Control).Values[TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1]] := String(XMLDoc.DocumentElement.Attributes['id'])
else
TValuelistEditor(Control).Values[TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1]] := '<Empty>';
end else begin
MessageBeep(0);
FlashWindow(Application.Handle, True);
ShowMessagePos('...');
end;
finally
XMLDoc.Active := False; Result := 'Forced ' + Form1.RAWInputBtn.Caption + ' in ' + DateTimeToStr(Now);
end;
except
on E : EXMLDocError do
begin
Result := 'Forced ' + Form1.RAWInputBtn.Caption + ' in ' + DateTimeToStr(Now);
end;
end;
The problem is that I get access violations every time code goes into the line:
TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1] := XMLDoc.DocumentElement.NodeName;
I have tried various typecasts, values, parameters .. nothing does the trick.
What is my mistake?
I'm using Delphi XE.
As Ken commented your problem is, instead of finding the value list editor, you are finding your form and then typecasting it to a value list editor, hence the AV.
First, you're passing 'TForm' as 'lpClassName' to FindWindow. Assuming 'TForm' is the class name of your form, it will of course find the form - not a child window on it. Second, you cannot use FindWindow to find a child window, see its documentation, it searches top-level windows.
If you had tested the return of FindControl, the code raising the AV would never run:
if (Control <> nil) and (Control is TValueListEditor) then
You can use FindWindowEx to search in child windows, if you don't know the handle of your form find it first as you've done already:
FormHandle := FindWindow('TForm',PChar('(' + ExtractFileExt(Form1.Edit1.Text) + ')'));
if FormHandle <> 0 then
begin
Control := FindControl(FindWindowEx(FormHandle, 0, 'TValueListEditor', nil));
or better yet, test the return of FindWindowEx first to avoid passing '0' to FindControl:
ValueListEditorHandle := FindWindowEx(FormHandle, 0, 'TValueListEditor', nil);
if Win32Check(ValueListEditorHandle <> 0) then
begin
Control := FindControl(ValueListEditorHandle);
if Assigned(Control) then
begin
...
If your dynamically created form is part of the same application, you don't need all the noise of the incorrect FindControl(FindWindow()). Just create your form, giving it a name, and making Application the owner:
MyForm := TMyForm.Create(Application);
MyForm.Name := 'MyDynamicForm';
When you want to get a new reference to it:
var
TheForm: TMyForm;
i: Integer;
begin
TheForm := nil;
for i := 0 to Screen.FormCount - 1 do
if Screen.Forms[i] is TMyForm then
// Could also use Screen.Forms[i].Caption
if Screen.Forms[i].Name = 'MyDynamicForm' then
TheForm := TMyForm(Screen.Forms[i]);
if Assigned(TheForm) then
TheForm.MethodThatLoadsXML(XMLFileName); // or whatever
end;
TheForm.MethodThatLoadsXML can now access the TValueListEditor directly:
procedure TMyForm.MethodThatLoadsXML(const XMLFileName: string);
begin
// Load xml as before, using XMLFileName
with TValueListEditor.Create(Self) do
begin
Options := [Whatever];
Parent := Self;
Left := SomeNumber;
Top := SomeNumber;
// Create items for value list from XML and other stuff
end;
end;