unit pixel_classes;

interface

uses
 System.SysUtils, Rtti, TypInfo;

{$TYPEINFO ON}

type
 TZString = type string;

type
  TNullValue = record
  end;

  TNullable<T{: record}> = record
  public
    Value: T;
    HasValue: Boolean;

    class operator Implicit(const AValue: T): TNullable<T>;
    class operator Implicit(const AValue: TNullable<T>): T;
    class operator Implicit(const AValue: TNullValue): TNullable<T>;
    class operator Explicit(const AValue: T): TNullable<T>;

    // add these...
    class operator Equal(const A: TNullable<T>; const B: TNullValue): Boolean;
    class operator NotEqual(const A: TNullable<T>; const B: TNullValue): Boolean;
  end;

var NullValue: TNullValue;

implementation

class operator TNullable<T>.Implicit(const AValue: T): TNullable<T>;
begin
  Result.Value := AValue;
  Result.HasValue := True;
end;

class operator TNullable<T>.Implicit(const AValue: TNullable<T>): T;
begin
  if AValue.HasValue then
    Result := AValue.Value
  else
    Result := Default(T); // or raise an exception
end;

class operator TNullable<T>.Implicit(const AValue: TNullValue): TNullable<T>;
begin
  Result.Value := Default(T);
  Result.HasValue := False;
end;

class operator TNullable<T>.Explicit(const AValue: T): TNullable<T>;
begin
  Result.Value := AValue;
  Result.HasValue := True;
end;

class operator TNullable<T>.Equal(const A: TNullable<T>; const B: TNullValue): Boolean;
begin
  Result := not A.HasValue;
end;

class operator TNullable<T>.NotEqual(const A: TNullable<T>; const B: TNullValue): Boolean;
begin
  Result := A.HasValue;
end;

end.


-----------

unit orm_binding;

interface

uses
  System.Classes,
  System.Rtti,
  System.Generics.Collections,
  orm_utils,
  AdvCGrid,
  Variants,
  SysUtils,
  StrUtils,
  Dialogs,
  pixel_classes,
  interposer_class;

type
  /// <summary>
  /// Manages the binding between a TAdvColumnGrid and a TQryRecArray<TQryRecInfo> for bidirectional synchronization.
  /// </summary>
  TORMBinding = class
  private
    FGrid: TAdvColumnGrid;
    FData: TQryRecArray<TQryRecInfo>;
    FColumnFields: TDictionary<Integer, string>; // Maps grid column indices to ORM property names
    FColumnTypes: TDictionary<string, TRttiType>; // Maps ORM property names to their RTTI types
    FRowMapping: TDictionary<Integer, TQryRecInfo>; // Maps grid row indices to ORM records
    FCtx: TRttiContext; // RTTI context for dynamic property access

    /// <summary>
    /// Binds grid columns to ORM properties based on the ORMField property.
    /// </summary>
    procedure BindColumns;

    /// <summary>
    /// Populates the grid with data from TQryRecArray and maps each row to its corresponding ORM record.
    /// </summary>
    procedure PopulateAndMapGrid;

    /// <summary>
    /// Sets up event handlers for synchronizing changes between the grid and ORM data.
    /// </summary>
    procedure SetupEventHandlers;

    /// <summary>
    /// Event handler triggered when a grid cell edit is completed.
    /// Updates the corresponding ORM property.
    /// </summary>
    procedure GridCellChanged(Sender: TObject; ACol, ARow: Integer);

    /// <summary>
    /// Event handler triggered when an ORM property is changed.
    /// Updates the corresponding grid cell.
    /// </summary>
    procedure ORMPropertyChanged(Sender: TQryRecInfo; const PropertyName: string);

    /// <summary>
    /// Sets the ORM property based on the grid cell change.
    /// </summary>
    procedure SetORMProperty(ORMRec: TQryRecInfo; const FieldName: string; const Value: TValue);

    /// <summary>
    /// Updates a specific grid cell based on ORM changes.
    /// </summary>
    procedure UpdateGridCell(ACol: Integer; ARow: Integer);
  public
    /// <summary>
    /// Creates an instance of TORMBinding and initializes the binding between the grid and data array.
    /// </summary>
    constructor Create(AGrid: TAdvColumnGrid; AData: TQryRecArray<TQryRecInfo>);

    /// <summary>
    /// Frees resources and unsubscribes from events.
    /// </summary>
    destructor Destroy; override;

    /// <summary>
    /// Initializes the binding by populating the grid and setting up mappings.
    /// </summary>
    procedure InitializeBinding;
  end;

implementation

{ TORMBinding }

constructor TORMBinding.Create(AGrid: TAdvColumnGrid; AData: TQryRecArray<TQryRecInfo>);
begin
  inherited Create;
  FGrid := AGrid;
  FData := AData;
  FColumnFields := TDictionary<Integer, string>.Create;
  FColumnTypes := TDictionary<string, TRttiType>.Create;
  FRowMapping := TDictionary<Integer, TQryRecInfo>.Create;
  FCtx := TRttiContext.Create;

  BindColumns;
  InitializeBinding;
  SetupEventHandlers;
end;

destructor TORMBinding.Destroy;
var
  ORMRec: TQryRecInfo;
begin
  // Unsubscribe from ORM property changed events to prevent memory leaks
  for ORMRec in FRowMapping.Values do
    ORMRec.OnPropertyChanged := nil;

  FRowMapping.Free;
  FColumnFields.Free;
  FColumnTypes.Free;
  FCtx.Free;
  inherited;
end;

/// <summary>
/// Binds grid columns to ORM properties based on the ORMField property.
/// </summary>
procedure TORMBinding.BindColumns;
var
  i: Integer;
  PropName: string;
  RttiProp: TRttiProperty;
  ORMRec: TQryRecInfo;
begin
  if FData.RecCount = 0 then
    Exit; // No data to bind

  ORMRec := FData.Rec(0); // Assuming all records are of the same type

  for i := 0 to FGrid.ColCount - 1 do
  begin
    PropName := FGrid.ORMField[i];
    if PropName <> '' then
    begin
      if not FColumnFields.ContainsKey(i) then
        FColumnFields.Add(i, PropName)
      else
        FColumnFields[i] := PropName; // Update if already exists

      // Use RTTI to get the property type
      RttiProp := FCtx.GetType(ORMRec.ClassType).GetProperty(PropName);
      if Assigned(RttiProp) then
        FColumnTypes.AddOrSetValue(PropName, RttiProp.PropertyType)
      else
        raise Exception.CreateFmt('Property "%s" not found in class "%s".', [PropName, ORMRec.ClassName]);
    end;
  end;
end;

/// <summary>
/// Initializes the binding by populating the grid and setting up row mappings.
/// </summary>
procedure TORMBinding.InitializeBinding;
begin
  PopulateAndMapGrid;
end;

/// <summary>
/// Populates the grid with data from TQryRecArray and maps each row to its corresponding ORM record.
/// </summary>
procedure TORMBinding.PopulateAndMapGrid;
var
  i, j: Integer;
  ORMRec: TQryRecInfo;
  PropName: string;
  RttiProp: TRttiProperty;
  Value: TValue;
begin
  // Set grid row count (+1 if there's a header row; adjust as needed)
  FGrid.RowCount := FData.RecCount + 1;
  FRowMapping.Clear;

  for i := 0 to FData.RecCount - 1 do
  begin
    ORMRec := FData.Rec(i);
    FRowMapping.Add(i, ORMRec);

    // Subscribe to ORM property changed event
    ORMRec.OnPropertyChanged := ORMPropertyChanged;

    for j := 0 to FGrid.ColCount - 1 do
    begin
      if FColumnFields.TryGetValue(j, PropName) then
      begin
        RttiProp := FCtx.GetType(ORMRec.ClassType).GetProperty(PropName);
        if Assigned(RttiProp) then
        begin
          Value := ORMRec.GetPropertyValue(PropName);
          case RttiProp.PropertyType.TypeKind of
            tkInteger, tkInt64:
              FGrid.Cells[j, i + 1] := Value.ToString; // Adjust for header row
            tkFloat:
              FGrid.Cells[j, i + 1] := FloatToStr(Value.AsExtended);
            tkString, tkUString, tkLString, tkWString:
              FGrid.Cells[j, i + 1] := Value.AsString;
            tkEnumeration:
              if RttiProp.PropertyType.Handle = TypeInfo(Boolean) then
                FGrid.Cells[j, i + 1] := IfThen(Value.AsBoolean, 'True', 'False')
              else
                FGrid.Cells[j, i + 1] := Value.ToString;
            tkVariant:
              FGrid.Cells[j, i + 1] := VarToStr(Value.AsVariant);
            else
              FGrid.Cells[j, i + 1] := Value.ToString;
          end;
        end
        else
          FGrid.Cells[j, i + 1] := ''; // Clear cell if property not found
      end
      else
        FGrid.Cells[j, i + 1] := ''; // Clear cell if no mapping exists
    end;
  end;
end;

/// <summary>
/// Sets up event handlers for synchronizing changes between the grid and ORM data.
/// </summary>
procedure TORMBinding.SetupEventHandlers;
begin
  // Assign the grid's OnEditCellDone event to handle cell changes
  FGrid.OnEditCellDone := GridCellChanged;
end;

/// <summary>
/// Event handler triggered when a grid cell edit is completed.
/// Updates the corresponding ORM property.
/// </summary>
procedure TORMBinding.GridCellChanged(Sender: TObject; ACol, ARow: Integer);
var
  NewValue: string;
  ORMRec: TQryRecInfo;
  PropName: string;
  RttiProp: TRttiProperty;
  TValueToSet: TValue;
begin
  // Ignore header row or invalid rows
  if ARow < 1 then Exit; // Assuming first row (0) is header

  if not FRowMapping.TryGetValue(ARow - 1, ORMRec) then Exit; // Adjust for header row
  if not FColumnFields.TryGetValue(ACol, PropName) then Exit;

  RttiProp := FCtx.GetType(ORMRec.ClassType).GetProperty(PropName);
  if not Assigned(RttiProp) then Exit;

  // Get the new value from the grid
  NewValue := FGrid.Cells[ACol, ARow];

  // Convert the string to the appropriate TValue based on property type
  case RttiProp.PropertyType.TypeKind of
    tkInteger:
      TValueToSet := TValue.From<Integer>(StrToIntDef(NewValue, 0));
    tkInt64:
      TValueToSet := TValue.From<Int64>(StrToInt64Def(NewValue, 0));
    tkFloat:
      TValueToSet := TValue.From<Extended>(StrToFloatDef(NewValue, 0.0));
    tkString, tkUString, tkLString, tkWString:
      TValueToSet := TValue.From<string>(NewValue);
    tkEnumeration:
      if RttiProp.PropertyType.Handle = TypeInfo(Boolean) then
        TValueToSet := TValue.From<Boolean>(SameText(NewValue, 'True') or SameText(NewValue, '1'))
      else
        TValueToSet := TValue.From<string>(NewValue);
    tkVariant:
      TValueToSet := TValue.FromVariant(NewValue);
    else
      TValueToSet := TValue.From<string>(NewValue); // Default to string for unsupported types
  end;

  // Update the ORM property using the integrated SetORMProperty procedure
  SetORMProperty(ORMRec, PropName, TValueToSet);
end;

/// <summary>
/// Event handler triggered when an ORM property is changed.
/// Updates the corresponding grid cell.
/// </summary>
procedure TORMBinding.ORMPropertyChanged(Sender: TQryRecInfo; const PropertyName: string);
var
  ARow: Integer;
  ACol: Integer;
  RttiProp: TRttiProperty;
  Value: TValue;
begin
  // Find the row corresponding to the ORM record
  ARow := -1;
  for var Key in FRowMapping.Keys do
  begin
    if FRowMapping[Key] = Sender then
    begin
      ARow := Key;
      Break;
    end;
  end;

  if ARow = -1 then Exit; // Record not found

  // Find the column corresponding to the property
  ACol := -1;
  for var Col in FColumnFields.Keys do
  begin
    if SameText(FColumnFields[Col], PropertyName) then
    begin
      ACol := Col;
      Break;
    end;
  end;

  if ACol = -1 then Exit; // Column not found

  RttiProp := FCtx.GetType(Sender.ClassType).GetProperty(PropertyName);
  if not Assigned(RttiProp) then Exit;

  // Get the new value from ORM
  Value := Sender.GetPropertyValue(PropertyName);

  // Update the grid cell based on the property type
  case RttiProp.PropertyType.TypeKind of
    tkInteger, tkInt64:
      FGrid.Cells[ACol, ARow + 1] := Value.ToString; // Adjust for header row
    tkFloat:
      FGrid.Cells[ACol, ARow + 1] := FloatToStr(Value.AsExtended);
    tkString, tkUString, tkLString, tkWString:
      FGrid.Cells[ACol, ARow + 1] := Value.AsString;
    tkEnumeration:
      if RttiProp.PropertyType.Handle = TypeInfo(Boolean) then
        FGrid.Cells[ACol, ARow + 1] := IfThen(Value.AsBoolean, 'True', 'False')
      else
        FGrid.Cells[ACol, ARow + 1] := Value.ToString;
    tkVariant:
      FGrid.Cells[ACol, ARow + 1] := VarToStr(Value.AsVariant);
    else
      FGrid.Cells[ACol, ARow + 1] := Value.ToString;
  end;
end;

/// <summary>
/// Sets the ORM property based on the grid cell change.
/// </summary>
procedure TORMBinding.SetORMProperty(ORMRec: TQryRecInfo; const FieldName: string; const Value: TValue);
var
  Ctx: TRttiContext;
  Prop: TRttiProperty;
  ORMFieldName: string;
  ObjType: TRttiType;
begin
  Ctx := TRttiContext.Create;
  try
    ObjType := Ctx.GetType(ORMRec.ClassType);
    Prop := ObjType.GetProperty(FieldName);

    if Assigned(Prop) and Prop.IsWritable then
    begin
      ORMFieldName := '_' + FieldName;

      if ObjType.GetField(ORMFieldName) = nil then
        Exit; // Backing field not found, skip

      if Prop.PropertyType.TypeKind = tkRecord then
      begin
        // Check if the property is a TNullable<T>
        if Prop.PropertyType.Name.StartsWith('TNullable<') then
        begin
          if Value.IsEmpty or VarIsNull(Value.AsVariant) then
          begin
            // Set the property to null
            if Prop.PropertyType.Name = 'TNullable<System.string>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<string>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Integer>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Integer>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Int64>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Int64>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Extended>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Extended>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Currency>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Currency>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.TDateTime>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<TDateTime>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Boolean>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Boolean>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Variant>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Variant>>(NullValue));
          end
          else
          begin
            // Set the property with the actual value, adjust types
            if Prop.PropertyType.Name = 'TNullable<System.string>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<string>>(Value.AsString))
            else if Prop.PropertyType.Name = 'TNullable<System.Integer>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Integer>>(Value.AsInteger))
            else if Prop.PropertyType.Name = 'TNullable<System.Int64>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Int64>>(Value.AsInt64))
            else if Prop.PropertyType.Name = 'TNullable<System.Extended>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Extended>>(Value.AsExtended))
            else if Prop.PropertyType.Name = 'TNullable<System.Currency>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Currency>>(Value.AsCurrency))
            else if Prop.PropertyType.Name = 'TNullable<System.TDateTime>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<TDateTime>>(Value.AsType<TDateTime>))
            else if Prop.PropertyType.Name = 'TNullable<System.Boolean>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Boolean>>(Value.AsBoolean))
            // No need to handle TNullable<Variant> since no conversion required except as fallback
            else
              raise EORMUtilsException.CreateFmt('Unsupported nullable type: %s', [Prop.PropertyType.Name]);
          end;
        end
        else
          raise EORMUtilsException.CreateFmt('Unsupported record type: %s', [Prop.PropertyType.Name]);
      end
      else
      begin
        // Handle non-nullable types
        case Prop.PropertyType.TypeKind of
          tkString, tkLString, tkWString, tkUString:
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsString);
          tkInteger:
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsInteger);
          tkInt64:
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsInt64);
          tkFloat:
            if Prop.PropertyType.Handle = TypeInfo(Currency) then
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsCurrency)
            else if Prop.PropertyType.Handle = TypeInfo(TDateTime) then
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), TValue.From<TDateTime>(Value.AsType<TDateTime>))
            else
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsExtended);
          tkEnumeration:
            if Prop.PropertyType.Handle = TypeInfo(Boolean) then
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsBoolean)
            else
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsString);
        else
          raise EORMUtilsException.CreateFmt('Unsupported type for field "%s" in record "%s".', [FieldName, ORMRec.ClassName]);
        end;
      end;

      // After setting the ORM property, find the associated component and update it
      var Component := ORMManager.FindComponent(ORMRec, FieldName);
      if Assigned(Component) then
      begin
        ORMManager.UpdateComponentFromORM(Component, ORMRec, FieldName);
      end;

    end
    else
      raise EORMUtilsException.CreateFmt('Property "%s" not found or not writable in record "%s".', [FieldName, ORMRec.ClassName]);
  finally
    // TRttiContext is a record and managed automatically
  end;
end;

end.

--------------
    


    Core Improvements:

    Introduce column behavior configuration (read-only, editable, custom edit)
    Add column metadata storage with display names and formatting options
    Implement event handlers for validation and editing control

    New Features:

    Filtering capability with real-time updates
    Sorting functionality by column
    CSV import/export functionality
    Record management (add, delete, move)
    Auto-generation of columns with customizable options

    Enhanced Type Support:

    Better handling of nullable types
    Improved type conversion and validation
    Support for custom edit masks
    Column alignment options

--------------------------------

unit orm_binding;

interface

uses
  System.Classes, System.Rtti, System.Generics.Collections, System.SysUtils,
  System.StrUtils, System.Variants, System.Math, System.TypInfo,
  Vcl.Dialogs, Vcl.StdCtrls, Vcl.Mask,
  AdvCGrid, AdvObj, BaseGrid,
  orm_utils, pixel_classes, interposer_class;

type
  TColumnBehavior = (cbReadOnly, cbEditable, cbCustomEdit);
  TColumnAlignment = (caLeft, caCenter, caRight);

  TColumnMetadata = class
    DisplayName: string;
    FormatString: string;
    Behavior: TColumnBehavior;
    Alignment: TColumnAlignment;
    EditMask: string;
    CustomEditClass: TControlClass;
    constructor Create;
  end;

  TValidationEvent = procedure(Sender: TObject; const Value: TValue; var IsValid: Boolean; var ErrorMessage: string) of object;
  TCustomEditEvent = procedure(Sender: TObject; ACol, ARow: Integer; var EditCtrl: TWinControl) of object;

  /// <summary>
  /// Manages the binding between a TAdvColumnGrid and a TQryRecArray<TQryRecInfo> for bidirectional synchronization.
  /// </summary>
  TORMBinding = class
  private
    FGrid: TAdvColumnGrid;
    FData: TQryRecArray<TQryRecInfo>;
    FColumnFields: TDictionary<Integer, string>;
    FColumnTypes: TDictionary<string, TRttiType>;
    FRowMapping: TDictionary<Integer, TQryRecInfo>;
    FColumnMetadata: TObjectDictionary<Integer, TColumnMetadata>;
    FCtx: TRttiContext;
    FOnValidate: TValidationEvent;
    FOnCustomEdit: TCustomEditEvent;
    FSortColumn: Integer;
    FSortAscending: Boolean;
    FFilterExpression: string;

    procedure BindColumns;
    procedure PopulateAndMapGrid;
    procedure SetupEventHandlers;
    procedure GridCellChanged(Sender: TObject; ACol, ARow: Integer);
    procedure ORMPropertyChanged(Sender: TQryRecInfo; const PropertyName: string);
    procedure SetORMProperty(ORMRec: TQryRecInfo; const FieldName: string; const Value: TValue);
    procedure UpdateGridCell(ACol, ARow: Integer);
    function ValidateValue(const Value: TValue; ACol: Integer): Boolean;
    procedure SetupColumnMetadata;
    procedure ApplyColumnMetadata;
    procedure HandleCustomEdit(Sender: TObject; ACol, ARow: Integer; var CanEdit: Boolean);
    procedure SortData;
    procedure ApplyFilter;
    function EvaluateFilterExpression(const Rec: TQryRecInfo): Boolean;
  public
    constructor Create(AGrid: TAdvColumnGrid; AData: TQryRecArray<TQryRecInfo>);
    destructor Destroy; override;
    procedure InitializeBinding;
    procedure RefreshGrid;
    procedure ExportToCSV(const FileName: string);
    procedure ImportFromCSV(const FileName: string);
    procedure AddRecord;
    procedure DeleteRecord(ARow: Integer);
    procedure MoveRecord(FromRow, ToRow: Integer);
    procedure AutoGenerateColumns(const Options: TStringList);
    procedure SetColumnMetadata(ACol: Integer; const DisplayName, FormatString: string;
      Behavior: TColumnBehavior; Alignment: TColumnAlignment; const EditMask: string = '';
      CustomEditClass: TControlClass = nil);
    procedure Sort(ACol: Integer);
    procedure Filter(const Expression: string);

    property OnValidate: TValidationEvent read FOnValidate write FOnValidate;
    property OnCustomEdit: TCustomEditEvent read FOnCustomEdit write FOnCustomEdit;
  end;

implementation

{ TColumnMetadata }

constructor TColumnMetadata.Create;
begin
  inherited;
  Behavior := cbEditable;
  Alignment := caLeft;
end;

{ TORMBinding }

constructor TORMBinding.Create(AGrid: TAdvColumnGrid; AData: TQryRecArray<TQryRecInfo>);
begin
  inherited Create;
  FGrid := AGrid;
  FData := AData;
  FColumnFields := TDictionary<Integer, string>.Create;
  FColumnTypes := TDictionary<string, TRttiType>.Create;
  FRowMapping := TDictionary<Integer, TQryRecInfo>.Create;
  FColumnMetadata := TObjectDictionary<Integer, TColumnMetadata>.Create([doOwnsValues]);
  FCtx := TRttiContext.Create;
  FSortColumn := -1;
  FSortAscending := True;

  BindColumns;
  SetupColumnMetadata;
  InitializeBinding;
  SetupEventHandlers;
end;

destructor TORMBinding.Destroy;
var
  ORMRec: TQryRecInfo;
begin
  for ORMRec in FRowMapping.Values do
    ORMRec.OnPropertyChanged := nil;

  FRowMapping.Free;
  FColumnFields.Free;
  FColumnTypes.Free;
  FColumnMetadata.Free;
  FCtx.Free;
  inherited;
end;

procedure TORMBinding.BindColumns;
var
  i: Integer;
  PropName: string;
  RttiProp: TRttiProperty;
  ORMRec: TQryRecInfo;
begin
  if FData.RecCount = 0 then
    Exit;

  ORMRec := FData.Rec(0);

  for i := 0 to FGrid.ColCount - 1 do
  begin
    PropName := FGrid.ORMField[i];
    if PropName <> '' then
    begin
      FColumnFields.AddOrSetValue(i, PropName);

      RttiProp := FCtx.GetType(ORMRec.ClassType).GetProperty(PropName);
      if Assigned(RttiProp) then
        FColumnTypes.AddOrSetValue(PropName, RttiProp.PropertyType)
      else
        raise Exception.CreateFmt('Property "%s" not found in class "%s".', [PropName, ORMRec.ClassName]);
    end;
  end;
end;

procedure TORMBinding.InitializeBinding;
begin
  PopulateAndMapGrid;
  ApplyColumnMetadata;
end;

procedure TORMBinding.PopulateAndMapGrid;
var
  i, j: Integer;
  ORMRec: TQryRecInfo;
  PropName: string;
  RttiProp: TRttiProperty;
  Value: TValue;
  Metadata: TColumnMetadata;
begin
  FGrid.RowCount := FData.RecCount + 1;
  FRowMapping.Clear;

  for i := 0 to FData.RecCount - 1 do
  begin
    ORMRec := FData.Rec(i);
    FRowMapping.Add(i, ORMRec);
    ORMRec.OnPropertyChanged := ORMPropertyChanged;

    for j := 0 to FGrid.ColCount - 1 do
    begin
      if FColumnFields.TryGetValue(j, PropName) then
      begin
        RttiProp := FCtx.GetType(ORMRec.ClassType).GetProperty(PropName);
        if Assigned(RttiProp) then
        begin
          Value := ORMRec.GetPropertyValue(PropName);
          if FColumnMetadata.TryGetValue(j, Metadata) then
          begin
            case RttiProp.PropertyType.TypeKind of
              tkInteger, tkInt64:
                FGrid.Cells[j, i + 1] := FormatFloat(Metadata.FormatString, Value.AsInt64);
              tkFloat:
                FGrid.Cells[j, i + 1] := FormatFloat(Metadata.FormatString, Value.AsExtended);
              tkString, tkUString, tkLString, tkWString:
                FGrid.Cells[j, i + 1] := Value.AsString;
              tkEnumeration:
                if RttiProp.PropertyType.Handle = TypeInfo(Boolean) then
                  FGrid.Cells[j, i + 1] := IfThen(Value.AsBoolean, 'True', 'False')
                else
                  FGrid.Cells[j, i + 1] := Value.ToString;
              tkVariant:
                FGrid.Cells[j, i + 1] := VarToStr(Value.AsVariant);
              else
                FGrid.Cells[j, i + 1] := Value.ToString;
            end;
          end
          else
            FGrid.Cells[j, i + 1] := Value.ToString;
        end
        else
          FGrid.Cells[j, i + 1] := '';
      end
      else
        FGrid.Cells[j, i + 1] := '';
    end;
  end;
end;

procedure TORMBinding.SetupEventHandlers;
begin
  FGrid.OnEditCellDone := GridCellChanged;
  FGrid.OnCanEditCell := HandleCustomEdit;
end;

procedure TORMBinding.GridCellChanged(Sender: TObject; ACol, ARow: Integer);
var
  NewValue: string;
  ORMRec: TQryRecInfo;
  PropName: string;
  RttiProp: TRttiProperty;
  TValueToSet: TValue;
  IsValid: Boolean;
  ErrorMessage: string;
begin
  if ARow < 1 then Exit;

  if not FRowMapping.TryGetValue(ARow - 1, ORMRec) then Exit;
  if not FColumnFields.TryGetValue(ACol, PropName) then Exit;

  RttiProp := FCtx.GetType(ORMRec.ClassType).GetProperty(PropName);
  if not Assigned(RttiProp) then Exit;

  NewValue := FGrid.Cells[ACol, ARow];

  case RttiProp.PropertyType.TypeKind of
    tkInteger: TValueToSet := TValue.From<Integer>(StrToIntDef(NewValue, 0));
    tkInt64: TValueToSet := TValue.From<Int64>(StrToInt64Def(NewValue, 0));
    tkFloat: TValueToSet := TValue.From<Extended>(StrToFloatDef(NewValue, 0.0));
    tkString, tkUString, tkLString, tkWString: TValueToSet := TValue.From<string>(NewValue);
    tkEnumeration:
      if RttiProp.PropertyType.Handle = TypeInfo(Boolean) then
        TValueToSet := TValue.From<Boolean>(SameText(NewValue, 'True') or SameText(NewValue, '1'))
      else
        TValueToSet := TValue.From<string>(NewValue);
    tkVariant: TValueToSet := TValue.FromVariant(NewValue);
    else TValueToSet := TValue.From<string>(NewValue);
  end;

  IsValid := ValidateValue(TValueToSet, ACol);
  if IsValid then
    SetORMProperty(ORMRec, PropName, TValueToSet)
  else
  begin
    ShowMessage(ErrorMessage);
    UpdateGridCell(ACol, ARow);
  end;
end;

procedure TORMBinding.ORMPropertyChanged(Sender: TQryRecInfo; const PropertyName: string);
var
  ARow, ACol: Integer;
begin
  for ARow := 0 to FRowMapping.Count - 1 do
  begin
    if FRowMapping[ARow] = Sender then
    begin
      for ACol := 0 to FColumnFields.Count - 1 do
      begin
        if SameText(FColumnFields[ACol], PropertyName) then
        begin
          UpdateGridCell(ACol, ARow + 1);
          Exit;
        end;
      end;
    end;
  end;
end;

procedure TORMBinding.SetORMProperty(ORMRec: TQryRecInfo; const FieldName: string; const Value: TValue);
var
  Prop: TRttiProperty;
  ORMFieldName: string;
  ObjType: TRttiType;
begin
  ObjType := FCtx.GetType(ORMRec.ClassType);
  Prop := ObjType.GetProperty(FieldName);

  if Assigned(Prop) and Prop.IsWritable then
  begin
    ORMFieldName := '_' + FieldName;

    if ObjType.GetField(ORMFieldName) = nil then
      Exit;

    if Prop.PropertyType.TypeKind = tkRecord then
    begin
      if Prop.PropertyType.Name.StartsWith('TNullable<') then
      begin
        if Value.IsEmpty or VarIsNull(Value.AsVariant) then
          SetNullableToNull(ORMRec, Prop)
        else
          SetNullableValue(ORMRec, Prop, Value);
      end
      else
        raise EORMUtilsException.CreateFmt('Unsupported record type: %s', [Prop.PropertyType.Name]);
    end
    else
    begin
      case Prop.PropertyType.TypeKind of
        tkString, tkLString, tkWString, tkUString:
          ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsString);
        tkInteger:
          ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsInteger);
        tkInt64:
          ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsInt64);
        tkFloat:
          if Prop.PropertyType.Handle = TypeInfo(Currency) then
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsCurrency)
          else if Prop.PropertyType.Handle = TypeInfo(TDateTime) then
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), TValue.From<TDateTime>(Value.AsType<TDateTime>))
          else
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsExtended);
        tkEnumeration:
          if Prop.PropertyType.Handle = TypeInfo(Boolean) then
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsBoolean)
          else
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsOrdinal);
      else
        raise EORMUtilsException.CreateFmt('Unsupported type for field "%s" in record "%s".', [FieldName, ORMRec.ClassName]);
      end;
    end;

    var Component := ORMManager.FindComponent(ORMRec, FieldName);
    if Assigned(Component) then
    begin
      ORMManager.UpdateComponentFromORM(Component, ORMRec, FieldName);
    end;
  end
  else
    raise EORMUtilsException.CreateFmt('Property "%s" not found or not writable in record "%s".', [FieldName, ORMRec.ClassName]);
end;

procedure TORMBinding.UpdateGridCell(ACol, ARow: Integer);
var
  ORMRec: TQryRecInfo;
  PropName: string;
  Value: TValue;
  Metadata: TColumnMetadata;
begin
  if not FRowMapping.TryGetValue(ARow - 1, ORMRec) then Exit;
  if not FColumnFields.TryGetValue(ACol, PropName) then Exit;

  Value := ORMRec.GetPropertyValue(PropName);

  if FColumnMetadata.TryGetValue(ACol, Metadata) then
  begin
    case FColumnTypes[PropName].TypeKind of
      tkInteger, tkInt64:
        FGrid.Cells[ACol, ARow] := FormatFloat(Metadata.FormatString, Value.AsInt64);
      tkFloat:
        FGrid.Cells[ACol, ARow] := FormatFloat(Metadata.FormatString, Value.AsExtended);
      tkString, tkUString, tkLString, tkWString:
        FGrid.Cells[ACol, ARow] := Value.AsString;
      tkEnumeration:
        if FColumnTypes[PropName].Handle = TypeInfo(Boolean) then
          FGrid.Cells[ACol, ARow] := IfThen(Value.AsBoolean, 'True', 'False')
        else
          FGrid.Cells[ACol, ARow] := Value.ToString;
      tkVariant:
        FGrid.Cells[ACol, ARow] := VarToStr(Value.AsVariant);
      else
        FGrid.Cells[ACol, ARow] := Value.ToString;
    end;
  end
  else
    FGrid.Cells[ACol, ARow] := Value.ToString;
end;

function TORMBinding.ValidateValue(const Value: TValue; ACol: Integer): Boolean;
var
  ErrorMessage: string;
begin
  Result := True;
  ErrorMessage := '';

  if Assigned(FOnValidate) then
    FOnValidate(Self, Value, Result, ErrorMessage);

  if not Result then
    ShowMessage(ErrorMessage);
end;

procedure TORMBinding.SetupColumnMetadata;
var
  i: Integer;
  Metadata: TColumnMetadata;
begin
  for i := 0 to FGrid.ColCount - 1 do
  begin
    Metadata := TColumnMetadata.Create;
    Metadata.DisplayName := FGrid.Cells[i, 0];
    Metadata.FormatString := '';
    Metadata.Behavior := cbEditable;
    Metadata.Alignment := caLeft;
    FColumnMetadata.Add(i, Metadata);
  end;
end;

procedure TORMBinding.ApplyColumnMetadata;
var
  i: Integer;
  Metadata: TColumnMetadata;
begin
  for i := 0 to FGrid.ColCount - 1 do
  begin
    if FColumnMetadata.TryGetValue(i, Metadata) then
    begin
      FGrid.Cells[i, 0] := Metadata.DisplayName;
      case Metadata.Alignment of
        caLeft: FGrid.Alignments[i] := taLeftJustify;
        caCenter: FGrid.Alignments[i] := taCenter;
        caRight: FGrid.Alignments[i] := taRightJustify;
      end;
      FGrid.ReadOnly[i] := Metadata.Behavior = cbReadOnly;
    end;
  end;
end;

---------------------------------




Based on the included information, please carefully review everything and implement all the missing code enhancements on orm_binding unit. Give me the enhanced orm_binding unit without any gaps.