I am having issues with a DevExpress VCL grid, throwing out a "RecordIndex out of range" error, despite not directly calling any record functions of the grid in that scenario.
What I am doing is fairly simple: once the record is changed(AfterScroll), a method is called. Inside this method, I call another method which assigns the data source and data fied name according to a field in the new record.
The code is pretty simple and goes like this:
procedure TValidatedOrders.UpdateDispenseNotes;
var Dataset : TDataSet;
GroupTypeFieldName : String;
DataSource : TDataSource;
DataFieldName : String;
GroupType : Integer;
procedure SetSpecsDataSource;
begin
DataSource := DMValidatedDispense.DSDispenseGroupSpecs;
DataFieldName := 'GLAZING_INSTRUCTIONS';
end;
procedure SetCLsDataSource;
begin
DataSource := DMValidatedDispense.DSDispenseGroupCLs;
DataFieldName := 'WEAR_INSTRUCTIONS';
end;
begin
// Step 1: Get the group type
If GetTopPage = cTopPageOrders Then
Dataset := DMValidatedDispense.CDSLabOrders
Else
Dataset := DMValidatedDispense.CDSLabDispenses;
GroupType := Dataset.FieldByName( 'GROUP_TYPE' ).AsInteger;
// Step 2: Assign the MemoDispenseNotes data binding appropriately
If GetTopPage = cTopPageOrders Then
Begin
case GroupType of
cOrderGroupSpecs, cOrderGroupFrame,
cOrderGroupLens, cOrderGroupGlazing: SetSpecsDataSource;
cOrderGroupCLs: SetCLsDataSource;
else SetSpecsDataSource;
end;
End
Else
Begin
case GroupType of
cDispenseGroupSpecs: SetSpecsDataSource;
cDispenseGroupCLs: SetCLsDataSource;
else SetSpecsDataSource;
end;
End;
MemoDispenseNotes.DataBinding.DataSource := DataSource;
MemoDispenseNotes.DataBinding.DataField := DataFieldName;
end;
The original code where the above method is called is just too long to report here, anyway it contains stuff like panels show/hide, checks of the type .FieldByName( 'GROUP_TYPE' ).AsInteger = SomeValue and so on: there's no locate, no FindKey or anything like that.
As I said, there's no record selection directly involved(not sure here what's going on inside the DevExpress grid though...) and I don't see why I'd be getting such error.
Anybody has got a brilliant idea of what could be going on?
Thank you very much!
AFAICS this method only changes the DataSource of a TcxDBMemo component. The culprit must be elsewhere and is most probably some event.
As a first step to debug it, I would try to use BeginUpdate and EndUpdate on the view of your grid at the beginning and end of the code.
Related
I have the following situation.
I made one component. I want to get some feedback from the component when I use one method.
Like component.Method(param1 : callback function);
all good and shiny so far.
the project have the following units.
formMain - visible form.
dataModule - working with the component.
form main need to pass to the datamodule one callback function to receive his feedback. this callback function is a different function from the component (has more params).
I dont know how to do this.
TFeedBackProcedure = procedure(param1 : Integer) of object;
TFeedBackProcedureByTypeOf = procedure(aTypeOf : Integer; param1 : Integer) of object;
// component
procedure Syncro(feedBack : TFeedBackProcedure);
begin
//somewhere inside
for i := 0 to 15 do begin
feedBack(i);
end;
end;
// the feeedback received so far when someone use the Syncro procedure is the value of i
// the datamodule
// inside of a method I need to pass also the i received from compoent also a typeof value used only inside datamodule
procedure UseMethod(feedbackProcedure : TFeedBackProcedureByTypeOf); // the extended callback
begin
typeof = 1;
if component.Syncro(???) then begin // <-------- how to ???
// do stuff
end;
end;
// the main form
// the real callback function
procedure TFormMain.Feddback(aTypeOf : Integer; param1: Integer);
begin
if aTypeOf = 0 then begin
label1.caption = IntToStr(param1);
end else begin
label2.caption = IntToStr(param1);
end;
end;
// usage of datamodule
procedure TFormMain.btn1Click(Sender: TObject);
begin
dataModule.UseMethod(Feddback);
end;
any ideas? any other methods to do this? (I also need this in FMX enviroment)
tks alot
Razvan
If you've written the component yourself then the easiest thing to do is to change the type declaration of TFeedbackProcedure so that it also accepts anonymous methods (as well as object methods):
TFeedBackProcedure = reference to procedure(param1 : Integer);
This lets you wrap the extended method in place and call it like so:
procedure UseMethod(feedbackProcedure : TFeedBackProcedureByTypeOf);
var
_typeOf : integer;
begin
_typeOf = 1;
component.Syncro(procedure(AParam : integer)
begin
feedbackProcedure(_typeOf, AParam);
end);
end;
I've simply shown calling the method here because your example writes .Synchro as if it were a function returning a boolean when, in fact, you've declared it as a simple procedure.
As an alternative, if you cannot change the method signature or add a wrapper to an existing class, you can always write a wrapper class to do the job. I've shown a dedicated wrapper class here, but you could just as easily add these fields and methods to any suitable class to wrap the functionality into an object method with the correct signature.
TCallbackContainer = class
private
FParam : integer;
FFeedbackProcByTypeOf : TFeedBackProcedureByTypeOf;
public
constructor Create(AProcByTypeOf : TFeedBackProcedureByTypeOf);
procedure WrapCallback(AParam:integer);
property IntParam : integer read FParam write FParam;
end;
with implementation :
constructor TCallbackContainer.Create(AProcByTypeOf : TFeedBackProcedureByTypeOf);
begin
FFeedbackProcByTypeOf := AProcByTypeOf;
end;
procedure TCallbackContainer.WrapCallback(AParam: Integer);
begin
FFeedbackProcByTypeOf(FParam, AParam);
end;
You can then call this like :
procedure UseMethod(feedbackProcedure : TFeedBackProcedureByTypeOf);
var
LCallbackContainer : TCallbackContainer;
begin
LCallBackContainer := TCallbackContainer.Create(feedbackProcedure);
try
LCallBackContainer.IntParam := 1;
component.Syncro(LCallbackContainer.WrapCallback);
finally
LCallBackContainer.Free;
end;
{ Or, make it FCallBackContainer and manage lifetime somehow...}
end;
Unlike with anonymous methods, which are reference counted, you have to manage the LCallbackContainer object lifetime here somehow. I've shown it as a local, which is fine if .Synchro is actually fully synchronous and you can free the callback container when it returns. If .Synchro is actually an async method, however, and returns before its work is complete then you need some way to manage the callback wrapper's lifetime.
You should also avoid naming a variable TypeOf since this will hide the standard method with that name. System.TypeOf is deprecated but it is still good practice to avoid naming conflicts like this.
I have a DevExpress grid where I would like to add an unbound checkbox to be able to select some of the items.
After the selection is made I press a button and I must loop the grid to get all the selected items.
It has to be a checkbox. I have tried with a multiselectable grid, but the users can't work with that.
I have tried all the samples that I have been able to find on the supportsites, but no luck.
I need the unbound approach since it is a multiuser setup and users have been selecting and deselecting for each other.
My question: does anyone have a working sample that shows how this can be done?
I've done this and it was (is!) pretty ugly! Create the grid view with bound columns and add an unbound checkbox column with a field type of boolean.
Basically I handle the OnCellClick of the grid view. I check if the item clicked is the checkbox column - by finding the first unbound column in the view with a checkbox type. Then I toggle its state.
I've set AutoEdit on the dataset to true but Deleting/Editing/Inserting to false and ImmediateEditor is false. Not exactly sure which of those are important.
I think the hardest thing was trying to fathom out the complex hierarchy of grid and view level objects and working out which levels contained which of the needed bits. I'm sure there's a better way of doing it but what we've got now works and I'm not going to touch it again!
This is lifted from my code but modified slightly and not tested as it stands - it also needs a bit more error checking:
procedure TMyForm.ViewCellClick(Sender: TcxCustomGridTableView;
ACellViewInfo: TcxGridTableDataCellViewInfo; AButton: TMouseButton;
AShift: TShiftState; var AHandled: Boolean);
var
col: TcxGridColumn;
begin
// Manually handle the clicking of the checkbox cell - otherwise it seems
// virtually impossible to get the checked count correct.
col := GetViewCheckColumn(Sender);
if (Sender.Controller.FocusedItem = col) then
begin
ToggleRowSelection(TcxCustomGridTableView(TcxGridSite(Sender).GridView), col);
end;
end;
procedure TMyForm.ToggleRowSelection(AView: TcxCustomGridTableView; ACol: TcxGridColumn);
var
rec: TcxCustomGridRecord;
begin
rec := AView.Controller.FocusedRecord;
if (rec = nil) then exit;
if (rec.Values[ACol.Index] = TcxCheckBoxProperties(ACol.Properties).ValueChecked) then
begin
rec.Values[ACol.Index] := TcxCheckBoxProperties(ACol.Properties).ValueUnchecked;
end
else
begin
rec.Values[ACol.Index] := TcxCheckBoxProperties(ACol.Properties).ValueChecked;
end;
end;
function TMyForm.GetViewCheckColumn(AView: TcxCustomGridView): TcxGridColumn;
var
index: integer;
vw: TcxCustomGridTableView;
item: TcxCustomGridTableItem;
begin
// We're looking for an unbound check box column - we'll return the first
// one found.
Assert(AView <> nil);
result := nil;
if (AView is TcxCustomGridTableView) then
begin
vw := TcxCustomGridTableView(AView);
for index := 0 to vw.ItemCount - 1 do
begin
item := vw.Items[index];
if (item.Properties is TcxCustomCheckBoxProperties) then
begin
if (item is TcxGridDBColumn) then
begin
if (TcxGridDBColumn(item).DataBinding.FieldName = '') then
begin
result := TcxGridColumn(item);
break;
end;
end;
end;
end;
end;
end;
I then extended it by checking for a SPACE bar press in the OnKeyUp of the grid and calling ToggleRowSelection and also similar for a double click on a row.
When iterating through the rows you can test if a row is checked using something like the following:
function TMyForm.GetViewIsRowChecked(AView: TcxCustomGridView; ARecord: TcxCustomGridRecord): boolean;
var
col: TcxGridColumn;
begin
result := False;
col := GetViewCheckColumn(AView);
if ((col <> nil) and (ARecord <> nil)) then
begin
result := (ARecord.Values[col.Index] = TcxCheckBoxProperties(col.Properties).ValueChecked);
end;
end;
I think that's it. I've dug it out of a large grid/view helper unit we've built up over a while. Oh, and it's currently working with Delphi 2010 with DXVCL v2011 vol 1.10.
Hope it helps.
I am not sure if I am doing this right. I have a list of objects in the listbox and need to use IndexOf to get an object's index in the list.
if AlarmListBox.items.indexOf(alrm.Tagname) = -1 then
alrm is an object of TAlarm class.
Based on a StackOverflow C# question (How Can I Get the Index of An Item in a ListBox?), I try to override GetHashCode and Equals method, but still it doesn't work right.
Overriden Method:
TAlarm = class(System.Object)
TagName:string;
private
protected
public
method Equals(obj:System.Object):Boolean; override;
method GetHashCode:Int32; Override;
end;
method TAlarm.Equals(obj: system.Object):Boolean;
begin
result := TAlarm(obj).Tagname.Equals(self.Tagname);
end;
method TAlarm.GetHashCode:Int32;
begin
result := self.GetHashCode;
end;
This is how I populate AlarmListBox:
AlmGrp:= new TAlarmGroup;
AlarmListBox.items.Add(AlmGrp);
Compiler compiles without any errors, but when I debug the program line by line it always returns -1 and these overridden methods are never called or fired.
Am I implementing these overrides correctly? If not, how should I override them?
Sample code or hints or clues will be appreciated. Thanks,
UPDATE: To David Heffernan and others who have commented or answered, I think the problem might be that I am passing in two different object as Rob's last comment states. I do populate Listbox (UI) with TAlarmGroup but pass in TAlarm into IndexOf, although they both are identical classes. This is probably my problem. What I am really trying to do is populate Listbox with TAlarmGroup objects and through listbox.indexof by passing in the string (Tagname) I search for the object location. That's how it is done on Delphi XE it works great. The code above is not the actual code. Once I clean up the confusion in my code, it will probably work without overriding the GetHashcode and Equals method.
UPDATE: I think, I have stumbled onto something here. On Delphi XE or below, ListBox (UI) provides a method called AddObject. It's parameters are a string and an object respectively. So, when I populated objects into listbox I also provided the string to go along with it. When I searched I passed in a string or the alarm group name. IndexOf searched on this string against the string it had for each object I provided and not against the object's field (TagName). In Delphi Prism, listbox doesn't have a similar method as AddObject method but only Add that only accepts object as a parameter.
Here's an example of doing what you want with the base TAlarm class you provided. I've also provided implementations of the overloaded Equals and GetHashCode that seem to work. (Again, I'm not a Prism/.NET developer; just trying to help out here.)
// In AlarmClass.pas
type
TAlarm = class(System.Object)
TagName:string;
private
protected
public
constructor;
method Equals(obj:System.Object): Boolean; override;
method GetHashCode:Int32; Override;
method ToString(): String; override;
end;
implementation
method TAlarm.GetHashCode: Int32;
begin
if Self = nil then
Result := inherited
else
Result := Self.TagName.GetHashCode;
end;
constructor TAlarm;
begin
inherited;
end;
method TAlarm.Equals(obj: System.Object): Boolean;
begin
if (obj = nil) or (GetType() <> obj.GetType()) then
Exit(False);
Result := TAlarm(obj).TagName.Equals(Self.TagName);
end;
method TAlarm.ToString(): String;
begin
Result := Self.TagName;
end;
// In MainForm.pas
method MainForm.button1_Click(sender: System.Object; e: System.EventArgs);
var
Idx: Integer;
begin
Idx := ComboBox1.SelectedIndex;
if Idx <> -1 then
ListBox1.SelectedIndex := ListBox1.Items.IndexOf(ComboBox1.Items[Idx]);
end;
method MainForm.MainForm_Load(sender: System.Object; e: System.EventArgs);
var
i, j: Integer;
Alarm: TAlarm;
aList: Array[0..4] of Object;
aFind: Array[0..1] of Object;
begin
j := 0;
for i := 0 to 4 do
begin
Alarm := new TAlarm;
Alarm.TagName := String.Format('Alarm{0}', i);
aList[i] := Alarm;
// Place items 1 & 3 in another array of searchable items -
// just for fun. Not suggesting implementing your app this way
// by any means.
if (i mod 2) > 0 then
begin
aFind[j] := Alarm;
Inc(j);
end;
end;
ListBox1.Items.AddRange(aList);
ComboBox1.Items.AddRange(aFind);
end;
Here's how it looks with an item selected in the ComboBox after clicking the Button:
I know what changed. I know why. But..
TComplicatedCallMaker = record
Param1: TRecordType;
Param2: TRecordType;
{...}
Param15: TRecordType;
procedure Call;
end;
function ComplicatedCall: TComplicatedCallMaker;
begin
{ Fill default param values }
end;
procedure DoingSomeWorkHere;
begin
with ComplicatedCall do begin
Param7 := Value7;
Param12 := Value12;
Call;
end;
end;
This has perfectly worked before Delphi 2010. An extremely useful technique for making calls which accept a load of parameters but usually only need two or three. Never the same ones though.
And now it gives... guess what?
E2064: Left side cannot be assigned to.
Can't this helpful new behavior be disabled somehow? Any ideas on how to modify the pattern so it works?
Because seriously, losing such a handy technique (and rewriting a bunch of code) for no apparent reason...
I find it a little surprising that this ever worked but since you say it did I'm sure you are right. I'd guess the change was made without consideration for record methods. Without the ability to call methods then this construct would be rather pointless.
Anyway, the compiler isn't going to let you off the hook on this one so you'll have to do this:
type
TRecordType = record end;
TComplicatedCallMaker = record
Param1: TRecordType;
procedure Call;
end;
function ComplicatedCall: TComplicatedCallMaker;
begin
{ Fill default param values }
end;
procedure DoingSomeWorkHere(const Value: TRecordType);
var
CallMaker: TComplicatedCallMaker;
begin
CallMaker := ComplicatedCall;
with CallMaker do begin
Param1 := Value;
Call;
end;
end;
I... think I did it
I hope Delphi developers see what they make their programmers do!
type
PCallMaker = ^TCallMaker;
TCallMaker = record
Param1: integer;
Param2: integer;
function This: PCallMaker; inline;
procedure Call; inline;
end;
function TCallMaker.This: PCallMaker;
begin
Result := #Self;
{ Record functions HAVE to have correct self-pointer,
or they wouldn’t be able to modify data. }
end;
procedure TCallMaker.Call;
begin
writeln(Param1, ' ', Param2);
end;
function CallMaker: TCallMaker; inline
begin
Result.Param1 := 0;
Result.Param2 := 0;
end;
procedure DoingSomeWorkHere;
var cm: TCallMaker;
begin
{Test the assumption that cm is consistent}
cm := CallMaker;
if cm.This <> #cm then
raise Exception.Create('This wasn''t our lucky day.');
{Make a call}
with CallMaker.This^ do begin
Param1 := 100;
Param2 := 500;
Call;
end;
end;
This works, preserves all the good points of the old version (speed, simplicity, small call overhead) but aren't there any hidden problems with this approach?
i have an ole Object created with (simple verion)
obj := CreateOleObject('foo.bar');
obj.OnResult := DoOnResult;
procedure TMyDM.DoOnResult(Res: olevariant);
which all works, the res variable has a function String[] GetAns()
which im calling like this
var
ans: array of string;
begin
ans := Res.GetAns;
end;
which again works.. except sometimes no array is returned, and then an exception is thrown.
as a temporary solution i have wrapped it in a empty try except block, which i know is bad. I have tried VarIsArray(Res.GetAns) but it still donst work if the result is null
What is the correct way check for the right result?
ps I have no control over the ole Object
Christopher try using the VarIsNull function
procedure TMyDM.DoOnResult(Res: olevariant);
var
ans: array of string;
begin
if not VarIsNull(Res) then
if not VarIsNull(Res.GetAns) then
begin
ans := Res.GetAns;
//do your stuff
end;
end;