FireDac FDQuery commit changed DataFields when browsing through results - delphi

I have a FDQuery bound to a FDConnection.
I am displaying the data on my form with DB Data-Aware components.
Whenever i use the FPQuery.Next, .Prior, ... it browses between the results.
Everything is working fine.
Except when i change a value (e.g. John -> Jane) and then use FPQuery.Next to get the next result it saves commits the changed value to the db even tho i didn't FDQuery1.CommitUpdates.
Is there a way to only save changed DataFields when the user presses the nbPost-Button or uses FDQuery1.CommitUpdates and NOT when browsing between results?
Thanks!

Like I said in a comment, the standard TDataset behaviour is to call its .Post method to save changes to the current row before navigating to another one. This happens in the routine TDataSet.CheckBrowseMode in Data.DB.Pas, which is called before any navigation action. This can't be changed without deriving a custom TDataset descendant.
(from Data.DB.Pas)
procedure TDataSet.CheckBrowseMode;
begin
CheckActive;
DataEvent(deCheckBrowseMode, 0);
case State of
dsEdit, dsInsert:
begin
UpdateRecord;
if Modified then Post else Cancel;
end;
dsSetKey:
Post;
end;
end;
Of course, a TDataSet has a BeforePost event, so it might be tempting to try and use that to cancel changes; however, the problem with BeforePost is how to determine the context in which it is being called, so as to be able to tell whether its being called from CheckBrowseMode rather than as a result of the user clicking the Save button.
The simple way around that is to catch the BeforeAction event of your DBNavigator, before it calls a navigation action on the dataset which will trigger the .Post:
procedure TForm1.DBNavigator1BeforeAction(Sender: TObject; Button:
TNavigateBtn);
var
Res : Integer;
DataSet : TDataSet;
begin
DataSet := DBNavigator1.DataSource.DataSet;
case Button of
nbFirst,
nbPrior,
nbNext,
nbLast: begin
if DataSet.State in [dsEdit, dsInsert] then begin
Res := MessageDlg('The current row has unsaved changes. Abandon them?', mtWarning, [mbYes, mbNo], 0);
if Res = mrYes then
DataSet.Cancel
else
DataSet.Post;
end;
end;
end;
end;

Good answer MartynA.
If you do not want to limit on navigator component and have such a check in general you can override TFDQuery.InternalPost like so:
procedure TFDQuery.InternalPost;
begin
if State in [dsEdit, dsInsert] then
begin
if MessageDlg('Save changes?', mtWarning, [mbYes, mbNo], 0) = mrNo then
Cancel();
end;
inherited;
end;

Related

DELPHI 10.4, receive output result via Broadcast of Barcode Scanner

I have a Sunmi L2s device, and I'm trying to receive the result of a barcode scan via a broadcast to an Android app. I would like to create an app that, when I push the hardware button for scan (orange button to the side of the phone), shows the barcode on a TLabel.Text in the app.
I've found code here on StackOverflow, but I can't make it receive the results, and I'm getting a message when the app starts that says "External exception 0".
I'm new to Delphi/Android development, so any help is welcome!
implementation
{$R *.fmx}
uses
FMX.Platform.Android, Androidapi.JNI.JavaTypes, Androidapi.JNI.Net,
Androidapi.JNI.Os, Androidapi.Helpers;
procedure TForm4.FormCreate(Sender: TObject);
var
AppEventService: IFMXApplicationEventService;
begin
if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationEventService, AppEventService) then
AppEventService.SetApplicationEventHandler(HandleAppEvent);
MainActivity.registerIntentAction(StringToJString('com.sunmi.scanner.ACTION_DATA_CODE_RECEIVED'));
TMessageManager.DefaultManager.SubscribeToMessage(TMessageReceivedNotification, HandleActivityMessage);
end;
procedure TForm4.HandleActivityMessage(const Sender: TObject; const M: TMessage);
begin
if M is TMessageReceivedNotification then
HandleIntentAction(TMessageReceivedNotification(M).Value);
end;
function TForm4.HandleAppEvent(AAppEvent: TApplicationEvent; AContext: TObject): Boolean;
var
StartupIntent: JIntent;
begin
Result := False;
if AAppEvent = TApplicationEvent.BecameActive then
begin
StartupIntent := MainActivity.getIntent;
if StartupIntent <> nil then
HandleIntentAction(StartupIntent);
end;
end;
function TForm4.HandleIntentAction(const Data: JIntent): Boolean;
var
JStr: JString;
begin
Result := False;
if (Data <> nil) and Data.getAction.equals(StringToJString('com.sunmi.scanner')) then
begin
JStr := Data.getStringExtra(StringToJString('Data'));
Label1.Text := JStringToString(JStr);
Invalidate;
end;
end;
end.
One issue I see that could be causing your "External exception" error is that in HandleIntentAction(), Data.getAction() can potentially return nil, which you are not checking for. Also, you need to compare the complete action name, not a prefix of it.
Change this:
Data.getAction.equals(StringToJString('com.sunmi.scanner'))
To this instead:
StringToJString('com.sunmi.scanner.ACTION_DATA_CODE_RECEIVED').equals(Data.getAction)
Other than that, the only other potential issue I see is in HandleAppEvent(), are you sure BecameActive is the best event to use to handle the StartupIntent? I would think FinishedLaunching would be a more appropriate event. An app can gain and lose focus multiple times during its lifetime, so you wouldn't want to handle the same startup Intent object over and over. Otherwise, at the very least, after you have processed the StartupIntent, you could optionally call MainActivity.setIntent(nil) so that MainActivity.getIntent() won't return the same Intent object anymore on subsequent events. Or, you could simply get rid of HandleAppEvent() and just handle the StartupIntent directly in your Form's OnCreate event.
The post is a bit old, however maybe you are querying the wrong key 'Data' instead of 'data' which seems to be the right default key for sunmi device :
JStr := Data.getStringExtra(StringToJString('Data'));
Replace it by
JStr := Data.getStringExtra(StringToJString('data'));

Get current field in TQuery to a TField

This is related to another question but doesn't really fit enough to include it with the original. When a Post is called, how can I get the field (or fields) that was modified to a TField?
For logging, I use the OnBeforePost event, which is called (as it says) just before the data is posted. The drawback to this, of course, is that your log table has to have fields wide enough to hold all possible content.
procedure TMyData.SomeTableBeforePost(DataSet: TDataSet);
var
i: Integer;
begin
for i := 0 to DataSet.FieldCount - 1 do
begin
// Skip calculated and lookup fields
if DataSet.Fields[i].FieldType = ftData then
begin
if DataSet.Fields[i].OldValue <> DataSet.Fields[i].NewValue then
begin
LogTable.Insert;
LogTableColumnName.AsString := DataSet.Fields[i].FieldName;
LogTableOldValue.Value := DataSet.Fields[i].OldValue;
LogTableNewValue.Value := DataSet.Fields[i].NewValue;
LogTable.Post;
end;
end;
end;
end;

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.

Proper Validation on TPageControl in Delphi

I'm working with Delphi 7 code to ensure that comments are entered on a tab have been saved before users can switch tabs.
The tabs are located on a TPageControl, and this code is triggered OnExit
procedure TfCallerInfo.tsChaplainExit(Sender: TObject);
begin
{ Compare the saved DB value with the text in the comments field }
if (dmMain.qChaplainCOMMENTS.AsString <> dbmChapComments.Text) then
begin
ShowMessage ('Please save the comments before proceeding.');
pcDetail.ActivePage := tsChaplain; // Remain on the Current Page
tsChaplain.SetFocus;
end;
end;
When users click on another tab tsInfoRequest for instance, the validation does trigger, but the Active Page becomes tsInfoRequest instead of remaining tsChaplain.
Any idea what I'm doing wrong?
There's probably a better way to do what you're trying to do. Use the TPageControl.OnPageChanging event instead.
procedure TfCallerInfo.pcDetailPageChanging(Sender: TObject;
NewPage: TTabSheet; var AllowChange: Boolean);
begin
if pc.ActivePage = tsChaplain then
begin
AllowChange := (dmMain.qChaplainCOMMENTS.AsString = dbmChapComments.Text);
if not AllowChange then
ShowMessage(...);
end;
end;
By the way, a better test might be
AllowChange := not dmMain.gChaplainCOMMENTS.Modified;
TField.Modified is set to True when the content of the field is changed when it's DataSet is in dsEdit or dsInsert mode, and set to False when it's state changes back to dsBrowse.

How do do things during Delphi form startup

I have a form one which I want to show a file open dialog box before the full form opens.
I already found that I can't do UI related stuff in FormShow, but it seems that I can in FormActivate (which I protect from being called a second time...)
However, if the user cancels out of the file open dialog, I want to close the form without proceeding.
But, a form close in the activate event handler generates an error that I can't change the visibility of the form.
So how does one do some UI related operation during form start up and then perhaps abort the form (or am I trying to stuff a function into the form that should be in another form?)
TIA
It would be best (i think) to show the file open dialog BEFORE you create and show the form. If you want to keep all code together you might add a public class procedure OpenForm() or something:
class procedure TForm1.OpenForm( ... );
var
O: TOpenDialog;
F: TForm1;
begin
O := TOpenDialog.Create();
try
// set O properties.
if not O.Execute then Exit
F := TForm1.Create( nil );
try
F.Filename := O.FIlename;
F.ShowModal();
finally
F.Free();
end;
finally
O.Free();
end;
end;
Set a variable as a condition of the opendialog and close the form on the formshow event if the flag is not set correctly.
procedure TForm1.FormCreate(Sender: TObject);
begin
ToClose := not OpenDialog1.Execute;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
if ToClose then Close();
end;
or even more simply
procedure TForm1.FormShow(Sender: TObject);
begin
if not OpenDialog1.Execute then Close();
end;
If you want to keep the logic conditioning the opening self-contained in the Form, you can put a TOpenDialog in your Form and use a code like this in your OnShow event:
procedure TForm2.FormShow(Sender: TObject);
begin
if OpenDialog1.Execute(Handle) then
Color := clBlue
else
PostMessage(Handle, WM_CLOSE, 0, 0); // NB: to avoid any visual glitch use AlpaBlend
end;
If you don't need this encapsulation, a better alternative can be to check the condition before trying to show the form, for instance by embedding the Form2.Show call in a function that tests all the required conditions first.
Two Ways....
1. using oncreate and onactivate
create a global flag or even 2
var
aInitialized:boolean;
Set the flag to false in the oncreate handler.
aInitialized := false; //we have not performed our special code yet.
Inside onActivate have something like this
if not aInitialized then
begin
//our one time init code. special stuff or whatever
If successful
then set aInitialized := true
else aInitialized := false
end;
And how to close it without showing anything just add your terminate to the formshow. of course you need to test for some reason to close.. :)
Procedure Tmaindlg.FormShow(Sender: TObject);
Begin
If (shareware1.Sharestatus = ssExpired) or (shareware1.Sharestatus = ssTampered) Then
application.Terminate;
End;
In your DPR you will need to add a splash screen type effect. In my case I am showing progress as the application starts. You could also just show the form and get some data.
Code from the splash.pas
Procedure tsplashform.bumpit(str: string);
Begin
label2.Caption := str;
gauge1.progress := gauge1.progress + trunc(100 / items);
update;
If gauge1.progress >= items * (trunc(100 / items)) Then Close;
End;
Program Billing;
uses
Forms,
main in 'main.pas' {maindlg},
Splash in 'splash.pas' {splashform};
{$R *.RES}
Begin
Application.Initialize;
Application.Title := 'Billing Manager';
SplashForm := TSplashForm.Create(Application);
SplashForm.Show;
SplashForm.Update;
splash.items := 5;
SplashForm.bumpit('Loading Main...');
Application.CreateForm(Tmaindlg, maindlg);
SplashForm.bumpit('Loading Datamodule...');
Application.CreateForm(TfrmSingleWorkorder, frmSingleWorkorder);
SplashForm.bumpit('Loading SQL Builder...');
Application.CreateForm(TDm, Dm);
SplashForm.bumpit('Loading Security...');
Application.CreateForm(TSQLForm, SQLForm);
SplashForm.bumpit('Loading Reports...');
Application.CreateForm(Tpickrptdlg, pickrptdlg);
Application.Run;
End.

Resources