delphitcxgrid

TcxGrid how to get modified value and update the next column's value?


I am using XE2. I want to do when a user edits the Unit Price in a grid, auto re-calculate total price which is the next column.

Which event I should use?

I tried TCXGridTableView OnEditValueChanged, but I cannot get the modified value? the AItem.EditValue is the before modifying value.

Then I tried TcxGridColumn OnEditValueChanged. I can get the modified value like this:

  cxCE := Sender as TcxCurrencyEdit;
  cxCE.Value; // this is the modified value

However, here I got an issue if user modified a value, then NOT pressing Enter, but press the TAB key to leave, a funny problem occurred:

  1. The TcxGridColumn OnEditValueChanged event does executed.
  2. I can still get the cxCE.Value (modified value) and updated the next column's value successful.
  3. Just after modifed the next column's value, cxCE.Value changed back to before modify value!
  4. as a result, user input is rolled back, but the next column does updated.

an example what happened:

Qty | Unit Price | Total Price

2...... 5................ 10

when user modifed unit price from 5 to 7, and press tab after OnEditValueChanged, Unit Price rolled back but my logic updated Total Price:

2...... 5 (rollback) 14(updated)

Appreciated if anyone could help me, thanks a lot.


Solution

  • Which event I should use?

    None of the above. I think you are going about this the wrong way. What you seem to want is to automatically update the TotalPrice field when the Qty or UnitPrice field changes. The most productive way to think of that is as a data-manipulation operation, rather that a GUI operation,and that's the way you should code it.

    The cxGrid is a db-aware component, and these are coded to automatically reflect changes to the data, so the way to go about updating the TotalPrice field is to do it in code which operates on the dataset, NOT in code which operates on the cxGrid. If you try to do it in code for the cxGrid, you'll find yourself continually "fighting" with the grid, because it knows how to be db-aware and you're trying, in effect, to subvert that.

    Try the example project below. Set up a new VCL project, add a TClientDataSet, TDataSource and TDBNavigator and "wire them up" in the usual way.

    Set up an OnCalcFields event handler for the CDS and an FormCreate event for the form and then add the code shown below.

    When the project runs, it dynamically creates a cxGRid to display the data (I did it this way because there are so many settings and sub-components in a cxGrid that it's easiest to create one in code rather than specify its settings in an answer like this).

    Play about with changing the values in the Qty and UnitPrice fields and notice that the TotalPrice automatically updates without requing any code which operates on the cxGrid.

    type
      TForm1 = class(TForm)
        CDS1: TClientDataSet;
        DS1: TDataSource;
        DBNavigator1: TDBNavigator;
        procedure FormCreate(Sender: TObject);
        procedure CDS1CalcFields(DataSet: TDataSet);
      private
      public
        cxGrid : TcxGrid;
        cxLevel : TcxGridLevel;
        cxView : TcxGridDBTableView;
      end;
    

    [...]

    // This is a utility function to create TFields in code
    function CreateField(AFieldClass : TFieldClass; AOwner : TComponent; ADataSet : TDataSet;
    AFieldName, AName : String; ASize : Integer; AFieldKind : TFieldKind) : TField;
    begin
      Result := AFieldClass.Create(AOwner);
      Result.FieldKind := AFieldKind;
      Result.FieldName := AFieldName;
      Result.Name := AName;
      Result.Size := ASize;
      Result.DataSet := ADataSet;
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    var
      i : Integer;
      Field : TField;
      Col : TcxGridDBColumn;
    begin
    
      //  First, create the Fields of the ClientDataSet
      Field := CreateField(TIntegerField, Self, CDS1, 'ID', 'CDS1ID', 0, fkData);
      Field := CreateField(TIntegerField, Self, CDS1, 'Qty', 'CDS1Qty', 0, fkData);
      Field := CreateField(TCurrencyField, Self, CDS1, 'UnitPrice', 'CDS1UnitPrice', 0, fkData);
      Field := CreateField(TCurrencyField, Self, CDS1, 'TotalPrice', 'CDS1TotalPrice', 0, fkInternalCalc);
    //  Field.ReadOnly := True;
    
      CDS1.CreateDataSet;
    
      CDS1.IndexFieldNames := 'ID';
    
      //  Next, populate the CDS with a few records
      //  Note : If we are using calculated fields, we do to need to specify
      //  a value for the TotalPriced field
      CDS1.InsertRecord([1, 1, 1]);
      CDS1.InsertRecord([2, 2, 5]);
      CDS1.InsertRecord([3, 3, 6]);
    
      CDS1.First;
    
      //  Now, create a cxGrid to display the CDS data
      cxGrid := TcxGrid.Create(Self);
      cxGrid.Parent := Self;
      cxGrid.Width := 400;
    
      cxLevel := cxGrid.Levels.Add;
      cxLevel.Name := 'Firstlevel';
    
      cxView := cxGrid.CreateView(TcxGridDBTableView) as TcxGridDBTableView;
      cxView.Name := 'ATableView';
      cxView.DataController.KeyFieldNames := 'ID';
      cxView.DataController.Options := cxView.DataController.Options + [dcoImmediatePost];
    
      cxLevel.GridView := cxView;
      cxView.DataController.DataSource := DS1;
      cxView.DataController.CreateAllItems;
    
      //  Since the TotalPrice column is a calculated field, we need to
      //  prevent the user from attempting to edit it
      Col := cxView.GetColumnByFieldName('TotalPrice');
      Col.Options.Editing := False;
    
      ActiveControl := cxGrid;
    
    end;
    
    //  Procedure to calculate the TotalPrice field
    procedure CalculateTotalPrice(DataSet : TDataSet);
    var
      Qty : Integer;
      UnitPrice,
      TotalPrice : Currency;
    begin
      Qty := DataSet.FieldByName('Qty').AsInteger;
      UnitPrice := DataSet.FieldByName('UnitPrice').AsCurrency;
      TotalPrice := Qty * UnitPrice;
      DataSet.FieldByName('TotalPrice').AsCurrency := TotalPrice;
    end;
    
    procedure TForm1.CDS1CalcFields(DataSet: TDataSet);
    begin
      CalculateTotalPrice(DataSet);
    end;