Recent

Author Topic: Keep user from moving off current record  (Read 21501 times)

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Keep user from moving off current record
« Reply #15 on: April 15, 2015, 11:00:24 am »
But somehow FPC doesn't allow access to a private-variable when the base-class isn't defined in the same unit (which Delphi does).
Is that a bug or intended behavior ??

Intended behaviour, for sure, since the access you desire is provided via the protected visibility specifier. Just because 'protected' has not been used in some particular case is no reason to water down the clear meaning of 'private'.

rvk

  • Hero Member
  • *****
  • Posts: 6748
Re: Keep user from moving off current record
« Reply #16 on: April 15, 2015, 11:18:29 am »
But somehow FPC doesn't allow access to a private-variable when the base-class isn't defined in the same unit (which Delphi does).
Is that a bug or intended behavior ??

Intended behaviour, for sure, since the access you desire is provided via the protected visibility specifier. Just because 'protected' has not been used in some particular case is no reason to water down the clear meaning of 'private'.
Ok, so it's intended to be different than Delphi. I would have thought it would be possible to do in {$mode delphi} but it's not. Is this not an example where the mode-switch could be used. This can be a problem when converting projects which use this functionality, but ok, it's a choice.

I couldn't find any clear reference of this difference (even on http://wiki.freepascal.org/Helper_types#Differences_between_mode_Delphi_and_ObjFPC).

But why is it still possible to access a private-variable via a class helper when the base-class is defined in the same unit?
For example... this works:
Code: [Select]
type
  TMyOrgClass = class
  private
    FMyPrivateProp: Integer;
  end;

  TMyClassHelper = class helper for TMyOrgClass
  private
    function GetMyPublicProp: Integer;
  public
    property MyPublicProp: Integer read GetMyPublicProp;
  end;

function TMyClassHelper.GetMyPublicProp: Integer;
begin
  Result:= Self.FMyPrivateProp;  // Access the org class members with Self
end;

If it is not intended to reach the private-variable in a base-class via a helper why does this example work when the base-class is in the same unit but not when the base-class is in a different unit? In that case private should mean private and you shouldn't be able to reach it, even in the same unit.


(Sorry if this gets this topic somewhat off-topic, but it's still about an attempt at resolving the original question via class-helpers)

rvk

  • Hero Member
  • *****
  • Posts: 6748
Re: Keep user from moving off current record
« Reply #17 on: April 15, 2015, 11:44:56 am »
If it is not intended to reach the private-variable in a base-class via a helper why does this example work when the base-class is in the same unit but not when the base-class is in a different unit? In that case private should mean private and you shouldn't be able to reach it, even in the same unit.
Never mind.

I see accessing a private variable from a class defined in the same unit is always possible (not only via a helper but even outside the class).
It's intended behavior:
Code: [Select]
type
  TMyOrgClass = class
  private
    FMyPrivateProp: Integer;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  a: TMyOrgClass;
begin
  a := TMyOrgClass.Create;
  a.FMyPrivateProp := 1;
end;

So it's just that Delphi also allows private-variable access via class-helpers from other units and Lazarus/FPC does not. (not even in {$mode delphi})

(this is still going to be a problem when converting existing code. it's not Delphi compatible behavoir)

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Keep user from moving off current record
« Reply #18 on: April 15, 2015, 12:12:35 pm »
I see accessing a private variable from a class defined in the same unit is always possible (not only via a helper but even outside the class).

This is the reason (I presume) why strict private was introduced.

rvk

  • Hero Member
  • *****
  • Posts: 6748
Re: Keep user from moving off current record
« Reply #19 on: April 15, 2015, 01:54:32 pm »
This is the reason (I presume) why strict private was introduced.
Strict, stricter, strictest :)

I have a form with a DBGrid and a DBNavigator on it.
dodgebros, does your code in Delphi only work when clicking the navigator? What happens when a user clicks away in the DBGrid?

If the TDBNavigator is the only control you want to control this behavior on your form, you can easily hide the navigationbuttons. The following is not ideal but it works:
Code: [Select]
procedure TForm1.DataSource1StateChange(Sender: TObject);
const
  NavigateOptions = [nbFirst, nbPrior, nbNext, nbLast];
begin
  if TDataSource(Sender).DataSet.State in [dsEdit, dsInsert] then
    DBNavigator1.VisibleButtons := DBNavigator1.VisibleButtons - NavigateOptions
  else
    DBNavigator1.VisibleButtons := DBNavigator1.VisibleButtons + NavigateOptions;
end;

But... there is another, more elegant option... :)

The Buttons from TDBNavigator are of type protected and they are accessible via a class helper.
For example, this would work:
Code: [Select]
type
  TMyDBNavigator = class helper for TDBNavigator
  public
    procedure DisableButtons(Buttonset: TDBNavButtonSet);
  end;

procedure TMyDBNavigator.DisableButtons(Buttonset: TDBNavButtonSet);
var
  CurButton: TDBNavButtonType;
begin
  for CurButton in Buttonset do
  begin
    Buttons[CurButton].Enabled := False;
    FocusableButtons[CurButton].Enabled := False;
  end;
end;

procedure TForm1.DataSource1StateChange(Sender: TObject);
begin
  if TDatasource(Sender).State in [dsInsert, dsEdit] then
    DBNavigator1.DisableButtons([nbFirst, nbPrior, nbNext, nbLast]);

  if TDatasource(Sender).State in [dsEdit] then
    DBNavigator1.DisableButtons([nbInsert, nbRefresh, nbDelete]);

  if TDatasource(Sender).State in [dsInsert] then
    DBNavigator1.DisableButtons([nbRefresh, nbDelete]);
end;

(This last example is more in line with your code but it's one step earlier as it disables the buttons so the user can't click them, so it's more clear to the user. But it only works for when the user wants to use the DBNavigator. Clicking another record in the DBGrid still works, but that was also the case in Delphi.)

dodgebros

  • Full Member
  • ***
  • Posts: 169
Re: Keep user from moving off current record
« Reply #20 on: April 15, 2015, 10:41:30 pm »
First off, thanks guys for trying to help me solve this problem.

In a previous post I said that my code did indeed work in Delphi XE.  To make sure I was remembering things correctly, I went back and checked; I was wrong, the code did not keep the user from moving off the record !  My humble apologies....

I will try the solutions suggested and report back shortly.

I also do a lot of MS Access VBA programming and by default Access has the same problem.  However, in Access you can check within the form's BeForeUpdate event to see if any of the form/fields are dirty and then set cancel=true to prevent the update.   Is there any way to do something similar in Lazarus?

TD

mangakissa

  • Hero Member
  • *****
  • Posts: 1131
Re: Keep user from moving off current record
« Reply #21 on: April 16, 2015, 08:40:32 am »
Off course you can.
Code: [Select]
procedure Tdatamodule.Query1BeforeScroll(DataSet: TDataSet);
begin
  if dataset.state in [dsInsert, dsEdit] then
    dataset.cancel
end;
Lazarus 2.06 (64b) / FPC 3.0.4 / Windows 10
stucked on Delphi 10.3.1

taazz

  • Hero Member
  • *****
  • Posts: 5368
Re: Keep user from moving off current record
« Reply #22 on: April 16, 2015, 08:57:28 am »
Off course you can.
Code: [Select]
procedure Tdatamodule.Query1BeforeScroll(DataSet: TDataSet);
begin
  if dataset.state in [dsInsert, dsEdit] then
    dataset.cancel
end;
woudln't that place the dataset in dsbrowse state?
Good judgement is the result of experience … Experience is the result of bad judgement.

OS : Windows 7 64 bit
Laz: Lazarus 1.4.4 FPC 2.6.4 i386-win32-win32/win64

balazsszekely

  • Guest
Re: Keep user from moving off current record
« Reply #23 on: April 16, 2015, 09:01:34 am »
Quote
@mangakissa
Off course you can.
Code: [Select]

procedure Tdatamodule.Query1BeforeScroll(DataSet: TDataSet);
begin
  if dataset.state in [dsInsert, dsEdit] then
    dataset.cancel
end;

How exactly your code will prevent the Dataset to scroll? Populate a grid, select then edit the first row. Click somewhere else, the dataset will scroll without any problem + you lose the edited data.

mangakissa

  • Hero Member
  • *****
  • Posts: 1131
Re: Keep user from moving off current record
« Reply #24 on: April 16, 2015, 10:58:43 am »
That's not his question in #20. If you want to cancel your updates without using post, this is the right way to do.
Lazarus 2.06 (64b) / FPC 3.0.4 / Windows 10
stucked on Delphi 10.3.1

taazz

  • Hero Member
  • *****
  • Posts: 5368
Re: Keep user from moving off current record
« Reply #25 on: April 16, 2015, 11:11:43 am »
That's not his question in #20. If you want to cancel your updates without using post, this is the right way to do.
you probably missunderstood what cancel does in access or me but I do not see the question has changed in #20.
Good judgement is the result of experience … Experience is the result of bad judgement.

OS : Windows 7 64 bit
Laz: Lazarus 1.4.4 FPC 2.6.4 i386-win32-win32/win64

balazsszekely

  • Guest
Re: Keep user from moving off current record
« Reply #26 on: April 16, 2015, 11:12:18 am »
Quote
@mangakissa
That's not his question in #20. If you want to cancel your updates without using post, this is the right way to do.
You're right, sorry!. It was a separate question, and your answer is correct. Please quote next time the question, it's much easier this way.
The original issues is still not solved though.

Quote
@taazz
you probably missunderstood what cancel does in access or me but I do not see the question has changed in #20.
In my opinion is indeed a separate question. The OP wants to cancel the latest edit. It has nothing to do with the scroll. The easiest way to implement this, is to post all the changes initially, then call Transaction.RollbackRetaining(if the user press/click the cancel button). The "post" is just a logical transaction, it's not saved permanently to database, until the commit method is not called.
« Last Edit: April 16, 2015, 11:26:35 am by GetMem »

rvk

  • Hero Member
  • *****
  • Posts: 6748
Re: Keep user from moving off current record
« Reply #27 on: April 16, 2015, 12:45:26 pm »
Okay... This will do what you want.
It uses (my earlier mentioned) class helper for your TDBNavigator. All buttons you don't want (for navigation) are disabled for clarity to the user. The new part is the variable AllowMoving (you might want to put that in Form1 instead of defining it globally). Assumption is that when the Dataset is in edit mode, there is always a .Post done when moving away from that record. Disallowing that post and issuing a SysUtils.Abort will trigger an silent exception and abort the moving of the dataset. The current records stays active and all your edit are kept. There is extra code in TDBNavigator (in BeforeAction) for when the Post-button is clicked. After that AllowMoving is set to true again.

So the user can only use the Post-button in the DBNavigator to post his changes and he can't click to another record before doing so.

Code: [Select]
type
  TMyDBNavigator = class helper for TDBNavigator
  public
    procedure DisableButtons(Buttonset: TDBNavButtonSet);
  end;

procedure TMyDBNavigator.DisableButtons(Buttonset: TDBNavButtonSet);
var
  CurButton: TDBNavButtonType;
begin
  for CurButton in Buttonset do
  begin
    Buttons[CurButton].Enabled := False;
    FocusableButtons[CurButton].Enabled := False;
  end;
end;

var
  AllowMoving: boolean = True;

procedure TForm1.DataSource1StateChange(Sender: TObject);
begin

  if TDatasource(Sender).State in [dsInsert, dsEdit] then
    DBNavigator1.DisableButtons([nbFirst, nbPrior, nbNext, nbLast]);

  if TDatasource(Sender).State in [dsEdit] then
    DBNavigator1.DisableButtons([nbInsert, nbRefresh, nbDelete]);

  if TDatasource(Sender).State in [dsInsert] then
    DBNavigator1.DisableButtons([nbRefresh, nbDelete]);

  // only disallow .post (and subsequent .next .prior etc) when dataset is in edit mode.
  AllowMoving := not (TDatasource(Sender).State in [dsInsert, dsEdit]);

end;

procedure TForm1.DBNavigator1BeforeAction(Sender: TObject;
  Button: TDBNavButtonType);
begin
  if Button in [nbPost, nbCancel] then
    AllowMoving := True;
end;

procedure TForm1.SQLQuery1BeforePost(DataSet: TDataSet);
begin
  // if moving off the current record is not allowed issue a "silent" exception
  // current record stays active and all your edits are kept
  // and the dataset stays in edit-mode

  if not AllowMoving then
    SysUtils.Abort;

end;

dodgebros

  • Full Member
  • ***
  • Posts: 169
Re: Keep user from moving off current record
« Reply #28 on: April 16, 2015, 09:09:09 pm »
Thanks rvk!  To see it in action I created a test form and your code works as advertised!  The public var thing is very similar to what I do in MS Access. 

I also came up with a way to solve my problem.  Please let me explain that I failed to mention in my first post that I do not like "inline editing" as in being able to change the contents of a cell of a row in a grid.  I prefer to launch a second form that allows a single record to be edited.

frmMain - has DBGrid1, DBNavigator1 on it.

frmEdit - has DBEdits set to same datasource as DBGrid1

Set DBNavigator1 to have only First, Next, Previous, Last, and Delete buttons.

Added button to frmMain to launch frmEdit.

The DBEdits on frmEdit are connected to the same Datasource as the DBGrid on frmMain so that if you select a particular record,  then open frmEdit, it will be on the same record.

On the OnCreate event of frmMain I have this code:
Code: [Select]
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  frmMain.DBGrid1.Options:=frmMain.DBGrid1.Options + [dgRowSelect];
  frmMain.DBGrid1.Options:=frmMain.DBGrid1.Options - [dgEditing];
end;

Then under the button that launches frmEdit I have this code:
Code: [Select]
procedure TfrmMain.btnEditClick(Sender: TObject);
begin
  DBGrid1.Options:=DBGrid1.Options - [dgRowSelect];
  DBGrid1.Options:=DBGrid1.Options + [dgEditing];
  DataModule1.DataSource1.DataSet.Edit;
  frmEdit.ShowModal;
end; 

Notice the ShowModal.  This takes care of the user being able to scroll the records in DBGrid1 with the mouse wheel.

Then under the Save button on frmEdit I have this code:
Code: [Select]
procedure TfrmEdit.btnCommitClick(Sender: TObject);
begin
    with DataModule1.SQLQuery1 do
    begin
      Edit;
      Upda]"]>Blockedde:=UpWhereChanged;
      ApplyUpdates;
      DataModule1.SQLTransaction1.Commit;
      Close;
      Open;
    end;
end;

Finally on the close event of frmEdit i have this code:
Code: [Select]
procedure TfrmEdit.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  frmMain.DBGrid1.Options:=frmMain.DBGrid1.Options + [dgRowSelect];
  frmMain.DBGrid1.Options:=frmMain.DBGrid1.Options - [dgEditing];
end;

I will work with both solutions to see which I like best. 

Thanks a million for all the help!!!
TD
« Last Edit: April 16, 2015, 09:13:15 pm by dodgebros »

rvk

  • Hero Member
  • *****
  • Posts: 6748
Re: Keep user from moving off current record
« Reply #29 on: April 16, 2015, 09:31:07 pm »
I prefer to launch a second form that allows a single record to be edited.
Yes... I was almost about to suggest the same when I came up with my other solution. I also never use the inline editing in a DBGrid. I myself find it not very intuitive for our users and a second form is always preferable.

I understand how you solved it. But I do have a few remarks for your solution.

1) You open the second edit-form with ShowModal. In that case there is no need to disable the scrolling in the DBGrid on the original form because the edit-form is on top and you can't switch away from it until you apply your edits and close the form. So the user can't even use the scroll of the DBGrid on the main form.

2) You could also create a new TSQLQuery and use that in the edit-form. When opening the edit-form you can give it the unique id of the record to be edited. Only downside is that when you close the edit-form you need to refresh the first form to re-read the changed record. (this could be done with a sendmessage)

3) You did a close/open in the edit-form. Was that actually necessary? You used the dataset from the main-form (with the DBGrid). Was a simple refresh of the DBGrid not enough to update the records there? Either way... doing a close/open will loose your position in the DBGrid when you return so you'll need to do some locate-code to set the cursor on the right spot again.

 

TinyPortal © 2005-2018