Wednesday, November 7, 2012
It is my special pleasure to announce that Bob Swart is joining me in presenting the annual Delphi Developer Days tours. Bob, known internationally as "Dr.Bob," is one of the world's leading Delphi experts and a dedicated supporter of the Delphi community.
Bob is singularly qualified to fill the vacancy created when Marco Cantú accepted the position of Delphi Product Manager. Like me, Bob is a Delphi developer, consultant, trainer, and author. His blog, "Dr. Bob Examines," has served as an invaluable lifeline to countless Delphi developers, and his Delphi courseware is highly regarded.
Bob and I have known each other for years. He has served as my editor for the articles that I write for the Software Development Network Magazine, and I have worked with him on numerous Delphi conferences. Bob has also been a featured guest speaker at Delphi Developer Days for the past several years, so he is intimately familiar with the format.
I am excited about this new opportunity to work together with Bob Swart on Delphi Developer Days. In the coming months, we will announce our 2013 topics, cities, and dates. We look forward to bringing to you the highest quality Delphi events, and invite your to visit www.DelphiDeveloperDays.com to learn more about these intense, two-day Delphi seminars.
Tuesday, November 6, 2012
I am not the first, nor will I be the last, but I want to wish Marco Cantú the very best in his new position as Delphi Product Manager at Embarcadero. This is great news for the Delphi community, and I am excited about what the future will bring.
I've known Marco since we first met in London in the mid-1990s at the annual Borland European conference, and have worked closely with him for more than five years organizing and delivering our annual Delphi Developer Days tours. I can attest to his commitment, knowledge, humor, and insight, and know that he will bring his many gifts to his new role.
As far as Delphi Developer Days is concerned, this is not the end, but rather a transition. I have exciting news to share about DDD, but that will have to wait until tommorow. For now, let me join in the chorus to extend a warm and hearty "congratulations" to both Marco and Embarcadero Technologies. May your future successes be ours as well.
Wednesday, August 22, 2012
Drag-and-drop operations, where the user drags data from one object and drops it into another object, has been a standard feature in Delphi applications for a long time. Is dragging and dropping with a DBGrid as the target more difficult than, say, a ListBox? Well, that depends.
If you are dragging from some source object and simply dropping that value into a DBGrid, without concern for where the dropped data will appear in the DBGrid, then no. From the DBGrid's OnDragDrop event you read the data from the source object and insert that data into the underlying DataSet. If the DataSet is displaying its data using an index, the dropped data will appear in the grid at the position dictated by the index key, otherwise it will appear at the end of the grid.
It's when you actually want to drop the source data into a particular row of the DBGrid that things become more complicated. It is even more complicated if you want to permit the user to be able to re-order the records in a DBGrid by dragging a record from one position in the DBGrid, dropping it into a new position.
Before I demonstrate how to implement sophisticated drag-and-drop operations using a DBGrid, let's first consider what makes this operation complicated. In a word, indexes. In most databases the order of records in a table is dictated by an index, and in most cases, a primary index. For the same reason, the sequence in which the records appear in a DBGrid are either affected by an index, or in the case of a query, an ORDER BY clause.
What this suggests is that you must manipulate the fields involved in the index or ORDER BY clause of your DataSet when implementing drag-and-drop in a DBGrid, which is correct. Following this logic, you might assume that with every drop operation you are going to write to your database. While you can do that, it is something that I personally want to avoid. Instead, I would prefer to implement the drag-and-drop functions on a cached version of the data, writing to the database only after the user is satisfied with the order of the data they are working with.
So, how is this done? With a ClientDataSet, of course.
Here's the basic approach. Associate a DBGrid with a ClientDataSet that makes use of an integer index that dictates the order of records in the grid. In some cases, including the code sample associated with this post, the ClientDataSet might be empty initially. In other cases, the ClientDataSet is first populated from a database.
When the data is loaded from a database, the field associated with the ordering index is most likely not actually a field in the database. Instead, it is an extra field added for ordering purposes. However, if your underlying data has a field that specifies order, you can use that. In my particular implementation it is necessary that this ordering field begin with 1 (the first record) and be sequential. This is just a detail, however, and the code that I will show could be modified to accommodate any kind of ordinal data.
Once you have a ClientDataSet with these features, all you need to do is to enable drag-and-drop and then implement OnDragOver and OnDragDrop event handlers from which you perform some basic operations. These techniques are demonstrated in the DBGridDragDrop project, which you can download from the preceding link. I wrote this project in Delphi XE2, and tested the posted code in Delphi 7. As a result, I assume that it will run in Delphi 7 Professional and later.
This project demonstrates two especially valuable drag-and-drop techniques. The first is dropping to an arbitrary position in the DBGrid. The second is dragging records from one position in the DBGrid to another.
There are two drag sources in this project, a ListBox and a DBGrid, and one target, the DBGrid. For demonstration purposes, the ListBox is loaded from a call to the PopulateListBox method, shown here:
procedure TForm1.PopulateListBox; begin ListBox.Clear; ListBox.Items.Add('One'); ListBox.Items.Add('Two'); ListBox.Items.Add('Three'); ListBox.Items.Add('Four'); ListBox.Items.Add('Five'); ListBox.Items.Add('Six'); ListBox.Items.Add('Seven'); ListBox.Items.Add('Eight'); ListBox.Items.Add('Nine'); ListBox.Items.Add('Ten'); end;
The ClientDataSet in this project begins as an empty DataSet. This ClientDataSet is created in the following method.
procedure TForm1.CreateClientDataSet; begin ClientDataSet := TClientDataSet.Create(Self); //This example assumes that the field defining the //order of records is named Sequence. This field //can appear in any position in the table structure. ClientDataSet.FieldDefs.Add('Sequence', ftInteger); ClientDataSet.FieldDefs.Add('Field', ftString, 30); //The ClientDataSet can have any number of fields //The following field is just for demonstration ClientDataSet.FieldDefs.Add('RandomNumber', ftInteger); ClientDataSet.CreateDataSet; ClientDataSet.IndexFieldNames := 'Sequence'; DataSource1.DataSet := ClientDataSet; end;
Both of these methods are called when the main form is first created, producing the form shown in the following figure.
Figure 1. The DBGridDragDrop project main form
We could set the DragMode property of both the ListBox and DBGrid to dmAutomatic, but I like to avoid that. Instead, I prefer to initiate the drag operation only after the mouse has been dragged some reasonable distance. Otherwise, a drag operation begins with the slightest mouse down drag. As a result, I initiate the drag operation from the OnMouseMove event handler of both drag sources. This event handler is shown here.
procedure TForm1.MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); const //this constant is typically declared with a higher scope MouseMovePixels = 15; begin if ssLeft in Shift then TListBox(Sender).BeginDrag(False, MouseMovePixels); end;
We need to add an OnDragOver event handler to the drop target in order to have the mouse cursor change to indicate that dropping is allowed (and to permit the OnDragDrop event handler to trigger). This event handler is shown here, and it is assigned only to the DBGrid.
procedure TForm1.DBGridDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin //Test for acceptable drag origin classes or objects Accept := (Source is TListBox) or (Source is TDBGrid); end;
Finally, the drop operation is performed from the target's OnDragDrop event handler. From here we determine the source of the drag operation (ListBox versus DBGrid) and take the appropriate action. Again, here is the event handler. It is only associated with the DBGrid.
procedure TForm1.DBGridDragDrop(Sender, Source: TObject; X, Y: Integer); var GridRow: Integer; OriginalRow: Integer; begin GridRow := DBGrid.MouseCoord(X,Y).Y; if GridRow = 0 then GridRow := 1; if (Source is TListBox) then begin //An item is being dropped into the DBGrid if ClientDataSet.IsEmpty then begin //The grid is empty. Add the item in the first position ClientDataSet.AppendRecord([1, TListBox(Source).Items[TListBox(Source).ItemIndex], RandomRange(1, 101)]); end else begin //Insert the item at the position of the drop if GridRow = -1 then //the drop is at the end of the DBGrid GridRow := ClientDataSet.RecordCount + 1 else //the drop needs to be inserted into the DBGrid. Make room ResequenceCDS(ClientDataSet, GridRow); //Insert the new item at the drop position ClientDataSet.InsertRecord( [GridRow, TListBox(Source).Items[TListBoxSource).ItemIndex], RandomRange(1, 101)]); end; //Remove the dropped item from the source (optional) TListBox(Source).Items.Delete(TListBox(Source).ItemIndex); end else if Source = Sender then begin //We are dragging within the DBGrid if ClientDataSet.IsEmpty then exit; OriginalRow := ClientDataSet.RecNo; if (OriginalRow = GridRow) or (GridRow = -1) then exit else MoveRecord(ClientDataSet, OriginalRow, GridRow); end; end;As you can see, one of the tricks to this technique is getting the row over which the drop operation occurred. This is done using the DBGrid's public MouseCoord method, to which we pass the X and Y coordinates, which we obtain from the event handler's parameters. We then use the Y property of the returned TGridCoord object to discover the record being dropped on. (MouseCoord is public in Delphi 7 and later. If you are using an earlier version where this method is protected, refer to Zarko's THackDBGrid to expose this method to your code.)
Dragging and Dropping to a Specific DBGrid PositionWhat happens next depends on the source of the drag operation. If the drag operation originated from a ListBox, we need to ensure that it is dropped into the proper position within the DBGrid. To do this, we have to check for three possible conditions.
If the ClientDataSet is empty, we simply insert a record, assigning a sequence of 1. If the integer Y property of the TGridCoord instance returned by MouseCoord evaluates to -1, the user has dropped at the end of the DBGrid. This, too, is a simple matter of inserting the record with a sequence of TClientDataSet.RecordCount + 1.
It is when the drop occurs somewhere in the middle of the DBGrid that the approach is more complicated. In short, we need to re-sequence all of the records appearing in the drop location, adding 1 to their sequence field value. After that we can insert the record using the drop position record number as the value for the sequence field. In the OnDragDrop event handler listed previously this task is handled first by a call to ResequenceCDS, after which the TClientDataSet's InsertRecord method is invoked.
It is in the ResequenceCDS method that things get fun. As you can see in the code listing that follows, a ClientDataSet cloned cursor is used to perform the record resequencing. This has the effect of changing the underlying indexed field values with a minimal impact on the user interface.
procedure TForm1.ResequenceCDS(cds: TClientDataSet; FromRow: Integer); var clone: TClientDataSet; SequenceFld: TField; begin clone := TClientDataSet.Create(nil); try clone.CloneCursor(cds, True); SequenceFld := clone.FieldByName('Sequence'); begin //Shift all records down to make room in the sequence //for the record being inserted clone.Last; while (SequenceFld.AsInteger >= FromRow) and not clone.bof do begin clone.Edit; SequenceFld.AsInteger := SequenceFld.AsInteger + 1; clone.Post; clone.Prior; end; end finally clone.Free; end; end;
The following three figures demonstrate this drag-and-drop in action. Figure 2 depicts the DBGrid after three records have been added, while Figure 3 shows a fourth record being dropped into the second position of the DBGrid. Figure 4 shows how the DBGrid appears following the drop operation.
Figure 2. Three records have been dropped onto the DBGrid from the ListBox.
Figure 3. A value from the ListBox is being dropped into the second position of the DBGrid
Figure 4. The drag-and-drop operation has dropped the new value in position 2
Dragging and Dropping Existing Records in a DBGridDragging an existing record from its current position to a new position within the DBGrid is a bit more involved. This process is handled by the MoveRecord method shown here.
procedure TForm1.MoveRecord(cds: TClientDataSet; OldPos, NewPos: Integer); var clone: TClientDataSet; SequenceFld: TField; begin clone := TClientDataSet.Create(nil); try clone.CloneCursor(cds, True); SequenceFld := clone.FieldByName('Sequence'); clone.RecNo := OldPos; clone.Edit; //Move the record being moved to the end of the sequence SequenceFld.AsInteger := cds.RecordCount + 1; clone.Post; if OldPos < NewPos then begin //Shift the records after the original old position up one position clone.RecNo := OldPos; while (clone.RecNo < NewPos) do begin clone.Edit; SequenceFld.AsInteger := SequenceFld.AsInteger - 1; clone.Post; clone.Next; end; end else begin //Shift the record before the original position down one position clone.IndexFieldNames := 'Sequence'; clone.RecNo := OldPos - 1; while (clone.RecNo >= NewPos) and (not clone.bof) do begin clone.Edit; SequenceFld.AsInteger := SequenceFld.AsInteger + 1; clone.Post; clone.Prior; end; end; //Move the record being moved to its new position clone.RecNo := cds.RecordCount; clone.Edit; SequenceFld.AsInteger := NewPos; clone.Post; finally clone.Free; end; cds.RecNo := NewPos; end;
As you can see from this code, we begin by repositioning the record that is being moved to the end of the index order, assigning to it RecordCount + 1 (in reality this could be a any value outside the current range of sequence numbers, just so long as we get the record we are moving out of the way. Next, depending on whether the move is upwards or downwards, we resequence each of the records that need to be repositioned before we perform the final movement. Finally, we reposition the record we are moving into its new position.
This operation is demonstrated in the following two figures. In Figure 5 the record at position 4 is being dragged to position 2. Figure 6 shows the DBGrid after the drop operation has been completed.
Figure 5. The record in position 4 is being dragged to position 2.
Figure 6. The dragged record has now been moved to position 2.
Deleting RecordsWe also have to update the sequence when a record is removed. Since the ReadOnly property is set to True on the DBGrid, record removal is something that we do from a custom popup menu. This popup menu has a single item, whose caption is Remove. This menu is surpressed when there are no records in the grid by the OnPopup event handler, shown here.
procedure TForm1.Remove1Click(Sender: TObject); var cds: TClientDataSet; begin cds := TClientDataSet(DBGrid.DataSource.DataSet); RemoveFromSequence(cds, cds.RecNo); end;
Removal is performed by the RemoveFromSequence method, shown here.
procedure TForm1.RemoveFromSequence(cds: TClientDataSet; Position: Integer); var clone: TClientDataSet; SeqFld: TField; begin clone := TClientDataSet.Create(nil); try clone.CloneCursor(cds, True); SeqFld := clone.FieldByName('Sequence'); clone.RecNo := Position; // Edit - Added this to next line: and (clone.RecordCount = 1) if (Position = 1) and (clone.RecordCount = 1) then begin //There is just one record. Delete it, //but do not try to set a new record position clone.Delete; end else begin if clone.RecNo = clone.RecordCount then begin clone.Delete; cds.RecNo := cds.RecordCount; end else begin clone.Delete; while not clone.eof do begin clone.Edit; SeqFld.AsInteger := SeqFld.AsInteger - 1; clone.Post; clone.Next; end; cds.RecNo := Position; end; end; finally clone.Free; end; end;
To Clone or Not To CloneAll of the methods that I have shown here make use of cloned cursors, which are very handy for working with a ClientDataSet in the background. However, cloned cursors are not essential for these techniques to work. In fact, the code sample in the download for this post includes two versions of the three workhorse methods (ResequenceCDS, MoveRecord, and RemoveFromSequence). The version that I have shown here that employes cloned cursors, and another version that does not. You might find that one version works better than the other, depending on the circumstances of your data.
SummaryDragging and Dropping is a common feature in Delphi applications, and a great technique for productive user interface design. Dragging and dropping within DBGrids, however, is more involved, especially if you want to drop somewhere other than the end of the DBGrid. In this posting, I have demonstrated how you can use a ClientDataSet and a cloned ClientDataSet cursor to implement sophisticated drag-and-drop operations involving a DBGrid as a target.
If you want to learn more about ClientDataSets, including an entire chapter on cloned cursors, please take a look at my most recent book Delphi in Depth: ClientDataSets.
Copyright © 2012 Cary Jensen. All Rights Reserved.
Monday, July 2, 2012
Some of Delphi's more powerful and best kept secrets have actually been in the product since the beginning, and in this short post I want to point out one of these, a feature that most Delphi developers I talk to are unaware of.
Last October I gave a keynote address in Düsseldorf, Germany, as part of the 15th annual Delphi Entwickler Konferenz (Developer Conference), generally known as EKON. The talk was titled The Evolution of Delphi: From Delphi to Delphi XE2, and I used the opportunity to reflect on the many groundbreaking innovations introduced in Delphi throughout the years, with one of the biggest innovations being Delphi itself.
Early in the talk I demonstrated the original version of Delphi (which was called Delphi, not Delphi 1, but the name Delphi 1 certainly makes it easier to convey what we are talking about when referring to the first release of this product). Seeing that original 16-bit application run again, albeit in 32-bit Windows 7, was fun and educational. (I had tried to create a virtual machine with Windows 3.11 for Workgroups, but alas, I tossed those disks during my last move.) Yes, Delphi 1 looks funny compared to the sleek and sophisticated stylings of the current product, but it really did have an amazing collection of capabilities.
Among the more profound features was runtime type information, a mechanism for generically examining the published members of objects, determining their names and types, and reading and writing their values. It was groundbreaking, and served as the foundation for the designer that changed the way we built our user interfaces.
But that is not the feature I want to talk about here. Instead, I want to bring attention to a often overlooked and greatly underused feature that can go a long way to making your life easier, at least as far as user interface design is concerned. This feature is a menu item called Lock Controls, and you can see it in the following figure from Delphi's (the original) Edit menu.
Lock Controls is a toggle menu item. Turn it on by select Edit | Lock Controls, and turn it off by selecting it again. When Lock Controls is on, you cannot change the positions of the components in the designer. Turn it off, and you can once again move and resize your controls.
Lock Controls is exceptionally valuable when you have a complex user interface with many controls precisely positioned, and you do not want to accidentally move anything as you select those components to adjust their properties or add event handlers. Once you have the components positioned where you need them, select Edit | Lock Controls to freeze the user interface. Now you can select individual components or groups of components without worrying about unintentionally moving them with your mouse.
Lock Controls applies to all forms, data modules, and frames, not just the one you are currently working on. As a result, you will need to turn it back off before you return to the task of user interface design.
Here is how Lock Controls looks today, in Delphi XE2. It's still in the same place, but works about the same as it did in the beginning. Actually, Lock Controls now prevents any change in position or size of controls. In pre-Delphi 2005 versions, you could still move or resize controls using your keyboard with the Ctrl and arrow keys. Nowadays, locking controls even prevents those operations.
Thursday, May 17, 2012
RAD Studio XE2 introduces a new mechanism for updating the user interface of applications in Delphi and C++Builder applications. This technology, which complements, but does not replace, the event driven model already provided by RAD Studio, enables developers with new and creative options for updating the user interface as data changes. The data affecting these changes may be associated with a database, permitting applications to view and edit this data, but just as easily can be associated with the changing state of objects used within the application.
Overview of LiveBindings
LiveBindings is a mechanism in RAD Studio for associating the properties of objects with expressions. These expressions can evaluate properties of the same object, properties of other objects, or complex expressions that include properties, literals, operators, and methods. Unlike traditional event handlers, which are custom code blocks that are invoked by the internal methods of objects, LiveBindings are string expressions whose runtime evaluation is invoked in response to a change in the state of an object.
LiveBindings play a crucial role in FireMonkey development, providing a general mechanism for binding user interface controls to DataSets. This is important since RAD Studio does not provide a collection of data-aware FireMonkey controls, as it does in the VCL (visual component library). Instead, LiveBindings permits any of FireMonkey's user interface controls to display data, and the same can be said about many of the VCL's non-data-aware controls, which can now also be bound to DataSets using LiveBindings.
On a broader level, however, LiveBindings provide RAD Studio developers with options not available in previous versions of RAD Studio, and serve as a basis for a new model of user interface synchronization. Similar to, but different than, data binding found in .NET, LiveBindings demonstrate Embarcadero's commitment to providing RAD Studio developers with leading edge technologies for software development.
An example of a LiveBinding is one that synchronizes two Edit controls, user interface objects that permit keyboard user input. One Edit may permit the user to enter the cost of an item in US dollars, and the other in euros. As the user enters the cost in dollars into the first Edit, the second Edit may update its display, displaying the equivalent cost in euros. The same LiveBinding can ensure that if the user enters the cost in euros into the second Edit, the first Edit updates its display in the equivalent dollars.
The conversion from dollars to euros can be performed by one expression associated with a LiveBinding, and the conversion from euros to dollars can be performed by another expression performed by that same LiveBinding. Nonetheless, the LiveBinding serves to keep the two Edit controls synchronized with each other, without involving an event handler or method that has any specific knowledge the Edit or of currency conversion.
The Expression Engine
Central to RAD Studio's support for LiveBindings is the expression engine introduced in RAD Studio XE2. The expression engine interprets string expressions at runtime, evaluating those expressions to a data type appropriate to the expression. While these strings are typically defined by the developer at design time, they can also be defined at runtime, which provides a level of flexibility previously unavailable.
Here is an example of a string expression:
'Record ' + ToStr(RecNo) + ' of ' + ToStr(RecordCount)
This particular expression is associated with a LiveBinding that is described in the RAD in Action white paper Understanding RAD Studio's LiveBindings. The object that is affected by this LiveBinding is a StatusBar component, a control that you can use to display information to the user, and which is typically displayed at the bottom of a Window. More specifically, this LiveBinding is designed to assign the value of the preceding expression to the SimpleText property of the StatusBar. The following figure shows the resulting expression in the running application.
A little more background is required before the preceding expression makes sense. LiveBindings often involve at least two objects, an output object, or ControlComponent, which is an object with two or more properties that will be assigned the value of one of more expressions. In this example, this object is a StatusBar, and the property being assigned is the StatusBar's SimpleText property.
The second object is the input object, or SourceComponent, and the properties and methods of this object can be combined with operators and literals to create one or more input expressions. In this case, the input object is a BindScopeDB, an object that exposes the RAD Studio's DataSet interface to the expression engine. RecNo is a property of the DataSet to which the BindScopeDB points, and it represents the position of the current record within a table of data. Likewise, RecCount is a property of the DataSet, and it identifies how many records are in the table of data. You can see the product of this expression in the StatusBar shown in the preceding figure.
This LiveBinding is somewhat simple, in that it displays read-only data. The StringGrid control, by comparison, is far more involved. An example of a StringGrid appears in the lower-right corner of the preceding figure, and the data displayed by that StringGrid is managed using a LiveBinding. This particular LiveBinding involves almost a dozen expressions, and these expressions work together to keep the StringGrid synchronized, bi-directionally, with a DataSet, which is a ClientDataSet in this particular example. This LiveBinding is an unmanaged LiveBinding, and no event handlers are needed to keep the data in the StringGrid synchronized with the DataSet.
A New Perspective
The LiveBindings that update the StatusBar and StringGrid represents a new way of synchronizing components in RAD Studio. Previously, the StatusBar would be updated by an event handler, such as the OnDataChange of a DataSource that points to the DataSet being displayed in the preceding figure. In that case, the event handler might look something like that shown in the following code:
procedure TForm1.OnDataChange(Sender: TObject); begin StatusBar1.SimpleText := 'Record ' + IntToStr(ClientDataSet1.RecNo) + ' of ' + IntToStr(ClientDataSet1.RecordCount); end;
While this event handler produces the same effect, updating the text displayed in the StatusBar, there is a very big difference between how this event handler performs the task and how it is done by the LiveBinding. In the case of the event handler, the event handler has to specifically name the object (the StatusBar) that is being affected, and to perform the assignment. If some other object, say the title of the form shown in the preceding figure, needed to be changed, the event handler text would have to look very different.
The LiveBinding used by the StatusBar is also an unmanaged LiveBinding, and it requires no event handler to perform its task. When the current record of the DataSet to which the BindScopeDB refers changes, the expression engine is called by the LiveBinding and the StatusBar's SimpleText property changes. The BindScopeDB knows nothing about the StatusBar. The StatusBar knows nothing about the BindScopeDB. The LiveBinding knows only those objects that it points to, and the expressions that are involved. And, it just works.
Another distinction between the OnDataChange event handler and the LiveBinding is that the event handler is compiled to binary code at compile time, and once compiled, will perform only those tasks whose need was anticipated by the developer at design time. The event handler is rigid, and any flexibility that is needed must be programmed into the event handler, leading to greater complexity.
LiveBindings perform their task by evaluating a string at runtime. This enables a whole new level of flexibility previous unavailable. For example, it is possible for the LiveBinding to obtain the string that it evaluates at runtime from the user interface, a database, or even from a service in the cloud. This new string could then be evaluated at runtime, permitting the LiveBinding to dynamically change the task it performs.
As mentioned, this LiveBinding is what is known as an unmanaged LiveBinding. As a general rule, unmanaged LiveBindings require no event handlers to work, relying instead on their implementation of the observer pattern, a technique in software development where one object registers its interest in another, so that the first object can stay informed about the second.
While the code to replace the StatusBar's display is easy to envision in code, the same cannot be said about writing event handlers to replace the LiveBinding used by the StringGrid. The StringGrid would require many, if not hundreds of lines of code to update its display, detect changes to its data, and write those changes back to the DataSet. There is simply no comparison between the simplicity offered by the LiveBinding and the complexity of the event handler approach.
There is another class of LiveBinding, called managed LiveBindings. Managed LiveBindings require that an object inform the expression engine every time something happens that requires that the expression be re-evaluated. These LiveBindings, while requiring an event handler, do not require that event handler to include any specific knowledge about what operations are being performed. They merely need to inform the expression engine that something has happened to an object, and these event handlers often look exactly like the following.
procedure TForm1.OnChange(Sender: TObject); begin BindingsList1.Notify(Sender, ''); end;
LiveBindings and Side Effects
The discussion to this point may have made it appear that LiveBindings and event handlers are mutually exclusive, but that is a gross oversimplification. Event handlers are compiled methods of an object, and those event handlers are executed by objects in response to a change in state or a user's interaction.
Just as objects can invoke methods within their scope, so can LiveBindings. Specifically, the string expression can include references to methods within the scope of the LiveBinding. These methods, being compiled code, execute very fast. In addition, these methods can do practically anything imaginable, from opening a database connection, to sending an email, to making extensive updates to the user interface.
These other tasks (sending an email, for example), when performed by accessor methods of properties (also referred to as property getters and setters), are referred to as side effects. Through this mechanism, changes to the property of an object may have, as an additional benefit, influences on other aspects of the object or the objects it uses. With LiveBindings, side effects can easily be extended beyond the object whose properties are being affected by the LiveBinding expressions. Specifically, while a method might return a value that is incorporated into the expression that the LiveBinding assigns to a property, that method can perform other tasks as a consequence of its execution. Side effects are an important feature of modern programming languages, and LiveBindings takes these effects to a whole new level.
RAD Studio XE2 introduces an important new technology that greatly expands the options that developers can use to manage the user interface. Not only do LiveBindings simplify the process of displaying database data, permitting almost any object to become data aware, they also permit dynamic expression evaluation that can produce significant side effects as a by product of the expression evaluation. This is but one of many new technologies that maintains RAD Studio's position as a leading software development tool.
Monday, April 30, 2012
As in the past, the response this year has been fantastic. It has been especially fun given all the new features that shipped with Delphi XE2. Marco's extensive exploration of the new FireMonkey component library has been one of the most favored tracks, and I have received a lot of positive feedback about my sessions on LiveBindings and DataSnap.
Our attendees have also given high marks to our examinations of Delphi's wide range of database capabilities and options for building Internet-based applications. There's also a healthy dose of iOS development, with examples of applications built for iPads (and iPhone) as well as Android devices.
But just because there's a lot of new features in Delphi doesn't mean that we're ignoring earlier versions. I have had consistently large audiences for my ClientDataSet and Unicode sessions. Likewise, Marco's session on RTTI (runtime type information) and Attributes, as well as his dbExpress Deep Dive session have also been high on many of our attendee's lists.
If you've been to one of our Delphi Developer Days events in the past, you already know about the course book that each attendee receives. The book contains papers for every one of the sessions that we present, which is especially valuable when you consider that half of our sessions are breakout sessions, meaning that you have two choose between Marco's presentation or mine. The extensive course book means that you don’t miss a thing.
This course book is almost 450 pages in length, and we're not talking about slide reproductions here. Each paper is narrative in style and contains detailed explanations and code samples. Indeed, the course book is larger than most Delphi books you can buy. Consider this, my last book, Delphi in Depth: ClientDataSets, which is 350 pages in length, contained around 85,000 words. Our Delphi Developer Days course book is almost 150,000 words in length. That's a big book, and its only available to attendees.
As I said, the response has been terrific. Not only have we sold out Frankfurt, but Chicago was sold out as well, and Amsterdam got close.
The Rome location is very convenient. It is a short 200 meters walk from the Termini Train station in central Rome, and there are trains from Roma Ciampino Airport (15 minutes travel time) and Leonardo da Vinci-Fiumicino Airport (30 minutes travel time) throughout the day.
In case you need additional reasons to come to Rome, here are two more. Daniele Teti of BitTime is our guest speaker in Rome. There he will present his talk titled "Improving Code Testability Through Dependency Injection," a talk I am looking forward to with great anticipation. And if that weren't enough, Marco Cantù will personally guide a walking tour of Rome for all who are interested after the first day of sessions.
For more information on Delphi Developer Days, and to register for Rome, please visit http://www.DelphiDeveloperDays.com. I hope to see you there!
Monday, February 27, 2012
I've been interested in LiveBindings since RAD Studio XE2 shipped. One obvious reason is that LiveBindings is important if you want to easily bind your FireMonkey controls to traditional Delphi DataSets. But there was something else, something that I had a hard time putting my finger on.
I remember David Intersimone (David I) asking me, prior to my 24 Hours of Delphi broadcast, to show LiveBindings used in a new and different way. I would have loved to, but I had yet to see such an example. I even blogged about this last month (http://caryjensen.blogspot.com/2012/01/whats-your-favorite-livebindings.html), asking readers to submit their favorite LiveBinding example that would open our eyes to the new way of doing things.
The response to that blog was interested. First of all, not one reader submitted a LiveBinding example. What I did get were some thoughtful comments about the limitations of LiveBindings, and these were similar to comments that I heard following my 24 Hours of Delphi presentation, as well as after a talk I gave at the Software Development Event in the Netherlands this past December.
Of these comments, the most common was that LiveBindings simply provide us with another way of doing something we already do, but in a more complicated way. Once again, I really felt like this opinion simply reflected the fact that we had not yet seen LiveBindings used in that way that would redefine, in our minds, what LiveBindings can do and how they can be used.
I recently wrote a white paper for Embarcadero on LiveBindings for their RAD in Action series. While doing so I had the time to reflect extensively on LiveBindings, including their limitations as they currently stand in this first release.
It was after I submitted the first draft for review that the example I am going to show came to me. In his technical edit, Jim Tierney pointed out that I failed to recognize that LiveBinding expressions could invoke methods of objects accessible in the input or output expression scope. I don't know why I overlooked this. He had mentioned this fact in one of his CodeRage 6 talks. I had simply forgotten. (And there is a trick. The method must uses parentheses, even if it has no parameters.)
As I re-wrote that particular section of the paper I had an inspiration. To be honest, this inspiration came at 2:00am in the morning, as inspirations often do. That pretty much ruined my night's sleep, as I could not wait to try what I now imagined a LiveBinding doing.
Here is the basic concept, and then I will show you a simple application. LiveBindings can produce side effects. That's it.
Sure, even this notion is not unique. I've seen several LiveBindings that assign a value to the ControlComponent's target property (the output expression), and that property has caused side effects (side effects being one of a property's magical features). But my idea, one that I had not seen before, was that you could invoke in your SourceComponent's SourceExpression (the input expression) a method that could, as part of its execution, produce a side effect.
Here is my simple example for your consideration. Take a look at this form, which is available in the code sample that accompanies my LiveBinding white paper.
procedure TForm1.Value1EditChange(Sender: TObject); begin BindingsList1.Notify(Sender, ''); end;
Let's now look at the Object Inspector, which shows the properties of the BindExpression LiveBinding.
The ControlComponent and the SourceComponent properties are both set to the ActionItem. The ControlExpression (the output expression) is Caption, a property of the ActionItem. The magic occurs in the input expression (SourceExpression). In this expression is a method invocation, which I've associated with the form (the ActionItem's owner). This method, OpenCDS, produces a side effect, the opening and closing of a ClientDataSet, as you can see in the following implementation.
function TForm1.OpenCDS(Open: Boolean): string; begin if Open then begin ClientDataSet1.Open; ClientDataSet1.LogChanges := False; exit('Close'); end else begin ClientDataSet1.SaveToFile; ClientDataSet1.Close; exit('Open'); end; end;
I am sure that more than a few people will be thinking to themselves that this is just another example of a LiveBinding that does something we can already do (open and close a ClientDataSet). For example, couldn't we have simply used the following OnExecute event handler?
procedure TForm1.Value1EditChange(Sender: TObject); begin if TActionItem(Sender).Caption = 'Open' then begin ClientDataSet1.Open; ClientDataSet1.LogChanges := False; TActionItem(Sender).Caption := 'Close'; end else begin ClientDataSet1.SaveToFile; ClientDataSet1.Close; TActionItem(Sender).Caption := 'Open'; end; end;
My response to has three parts. First, the second event handler, the one that explicitly opens and closes the ClientDataSet, needs to have intimate knowledge of what operation has to be performed. By comparison, the first event handler simply notifies the expression engine that something about the Sender has changed. The first event handler has no details about the change, nor does it specify what should happen in response. The expression engine does the actual assignment based on the LiveBinding, and the LiveBinding, not code, defines what happens.
Second, not all LiveBindings require that you notify the expression engine. Many of RAD Studio's LiveBindings, including Lists and Links, require no event handlers at all.
Third, for those LiveBindings that do require an event handler, all of them could potentially refer to this one, simple event handler, the one that calls the Notify method of the BindingsList. As a result, a form that uses LiveBindings to perform its various tasks may have zero or just one event handler. By comparison, if you performed those tasks using code, there would have to be many different event handlers, each one invoking its specific task and requiring specific knowledge about the operation they were designed to produce.
Basically what I am getting at is that this usage of a LiveBinding is profoundly different than normal event handler usage. The OnAction event handler is completely agnostic, as far as the operation that will result from its invocation. All of the behavior is defined declaratively in the LiveBinding, along with the actions defined in any methods that are invoked during the evaluation of the SourceExpression. What's even more exciting is that in the future this type of effect might be achieved without any event handlers at all.
But please do not get me wrong. I am not advocating that we should start replacing traditional event handlers with LiveBindings. That would be completely missing the point. LiveBindings have their place, and event handlers have their place as well.
Successful use of LiveBindings requires us to look at our goals from a different perspective. LiveBindings can do things that really don't fit into the event-driven model of traditional event handlers. Sure, in this version of RAD Studio they are somewhat limited, but that will change over time.
I have one final comment. Earlier in this posting I noted that invoking a method that produces side effects from a LiveBinding expression is similar to the power of side effects produced by property accessor methods. Actually, these two techniques are more closely related than you might think. When designing a new class, you might actually implement a side effect in a property accessor method, and a LiveBinding could then produce that side effect as a result of its assignment of data to the associated property.
On the other hand, side effects produced by property accessor methods are often associated with keeping the internals of that component consistent. By comparison, the types of side effects that you can introduce in methods invoked through LiveBindings can have a much more global impact, keeping many elements of a form, or an entire application, synchronized.
The white paper is available from Embarcadero's Web site at http://edn.embarcadero.com/article/42076.
I am also giving a Webinar on LiveBindings in the RAD in Action series on March 14th. You can learn more about this Webinar at http://www.embarcadero.com/rad-in-action/livebindings.
I am also doing a session on LiveBindings during my Delphi Developer Days 2012 tour with Marco Cantù. Learn more at http://DelphiDeveloperDays.com.
Monday, January 23, 2012
This is our fourth year that we've toured together, and in case you have not attended a Delphi Developer Days event, I wanted to share with you what we are doing in hope that you will consider attending.
Marco and I are both long time trainers and authors. Combined we have nearly 40 years of experience training professional developers, and have published close to 40 books. In putting together Delphi Developer Days, we strive to create a perfect balance of formal training, sharing, and networking.
To begin with, we limit the number of attendees in each city to a maximum of 35 developers. By doing so we retain the intimacy of a small group while generating the necessary synergy for the sharing of ideas and perspective. Personally, it gives us the opportunity to interact with each attendee without getting lost in the crowd.
There is also a little bit of conference flavor to Delphi Developer Days. In each city we have a keynote address from an Embarcadero representative, and also have a guest speaker who presents on an area of their own specialization. Our guest speakers this year include Stephen Ball (London), Bob Swart (Amsterdam), Ray Konopka (Chicago), Jim McKeeth (Washington DC/Baltimore), Bruno Fierens (Frankfurt), and Daniele Teti (Rome).
But make no mistake about it, the main goal is covering Delphi in depth. Over the course of the two days, there are more than fifteen technical sessions covering everything from the latest features in Delphi XE2 to classic topics such as Unicode migration and RTTI (runtime type information). There are also talks on Internet-related development, mobile applications, cross platform development, and traditional client/server topics. In some of the talks Marco and I present together, offering our individual insights on the topic, while for other sessions, we break out into separate rooms.
For the combined sessions, we have chosen topics that should be of interest to all Delphi developers. This year's combined sessions include a comparison of Delphi database development strategies, an overview of options for Internet development, and the always popular Tips, Tricks, and Techniques session. For the break out sessions, we have worked to create attractive options for creating your own agenda. For example, when a Delphi XE2-specific topic is being presented in one room, a classic topic that applies to many versions of Delphi is being covered in the other room.
Of course, this means that you might have to choose between two equally attractive topics. But we have that covered. Both Marco and I are prolific writers, and we commit to writing a paper for every talk we present. This means that if you have to skip a particular talk you will be able to read the associated paper. Over the past three years, the course book that each attendee to Delphi Developer Days receives has been several hundred pages in length, containing as much material as a book, and this year will be no different.
Delphi Developer Days takes a lot of planning, but it wouldn't be possible without our many sponsors. Both of last year's platinum sponsors, Embarcadero Technologies and Sybase (an SAP Company) have returned this year, as have all of last year's gold sponsors: /n software, DevArt, Gnostice Technologies, Programmer's Paradise, RemObjects Software, and TMS Software. In addition, this year we welcome two new gold sponsors: Fast Reports and Sisulizer. And of course, we value our more than 20 regular sponsors and Delphi user groups for their strong and continued support.
We are currently offering a ten percent discount early bird special if you register and make payment the deadline listed online. We also offer additional discounts for previous attendees of the Delphi Developer Days 2009 through 2011 tours. There is also a group discount when 3 or more employees of the same company register together. But don't wait too long to register. Our early registrations are on a record pace. Make sure you sign up now to guarantee your spot at Delphi Developer Days 2012.
For more information about Delphi Developer Days, including dates, topics, locations, and pricing, please visit http://www.delphideveloperdays.com/.
Thursday, January 12, 2012
One of the problems is that most of the demonstrations of LiveBindings are simple, in part because LiveBindings are so new. Another way to put this is that it's hard to think differently about object binding when we are so familiar with Delphi's existing mechanisms. As a result, most examples that I've seen so far duplicate much of what we already achieve in Delphi.
But this is bound to change. I believe that once we start to see creative applications of LiveBindings, we, the collective Delphi community, will begin to think about them differently.
I hope to jump start this process by collecting examples of LiveBindings that represent the way that we'll be using them in the future, and I'll publish these here. Of course, I'll give credit if you contribute so that you can bask in the gratitude of your fellow Delphi developers.
So, here is my question. Do you have examples of LiveBindings that go beyond the obvious? Alternatively, have you seen an example that breaks the mold? Is so, please share.
And, in case you haven't given much thought to LiveBindings, here is a short introduction.
LiveBindingsLiveBindings is a general term for Delphi's new object/property binding mechanism first introduced in RAD Studio XE2. It is the only binding mechanism available to the new FireMonkey cross-platform component library (FMX), and is also available for traditional visual component library (VCL) components.
At its core, LiveBindings is a mechanism for creating associations between objects and expressions. Expressions are strings that are evaluated by Delphi's new expression engine, and in the case of LiveBindings, they define what effect the expression will have on an object.
While expressions are strings, they are evaluated by the expression engine at runtime, which is quite a bit different than your Delphi code, which is compiled by Delphi's compiler at compile time. As a result, expressions are different from other string types you normally encounter in Delphi code. For one thing, expression strings can define literal values using either single quotes or double quotes. In addition, the expression engine recognizes special methods that have been registered with it through Delphi's Open Tools API (OTA), and can employ custom output converters to transform data from one type to another.
Another concept critical to LiveBindings is scope. In LiveBindings terminology, scope defines what is visible to the expression engine. Most LiveBindings require a control component (the object to which the expression will be applied), and in most cases a source component as well. In the case of these LiveBindings components, the control and source components are both in scope, making their properties available to the expression engine. Similarly, those custom methods that have been registered with Delphi from a design time package are also in scope, making those methods callable from your expressions.
It's worth noting that while LiveBindings use expressions, expressions can be used without LiveBindings. Specifically, you can create a scope programmatically, adding to it the objects and methods you want the expression engine to evaluate, and then ask the expression engine to perform the evaluation, returning a value based on the expression string. It's an important point, as far as the expression engine is concerned, but not something that you necessarily need to think about when you are using the LiveBindings components.
Do We Need LiveBindings?I recently spoke about LiveBindings during the "24 Hours of Delphi" broadcast with David Intersimone. One of the listeners asked a question about LiveBindings that I hear pretty often, though he gave a somewhat new twist. "Why do we need LiveBindings?" he asked. "After all, it appears that LiveBindings is just another way of doing what we already do using event handlers. It kind of seems like fishing poles. In older days we had cane fishing poles, and they worked just fine. The new fiberglass and graphite rods are nice, but they don't really do more than the old rods."
I like the analogy a lot, because it actually highlights why LiveBindings are a positive thing. Let's take the fishing pole example. A recent television show on The History Channel called "101 Gadgets that Have Changed the World," the publishers of the magazine Popular Mechanics list the top 101 devices that have had a dramatic impact on our daily lives. And, guess what, fiberglass fishing poles made the list (at 100), beating out duct tape and being edged out by the stapler.
In any case, the point is that while cane poles and fiberglass fishing rods perform the same task, they work differently, and fiberglass rods are functionally better on every level.
I think we are going to be saying the same thing about LiveBindings, once we get our heads around them. Yes, you can do many things with LiveBindings that can be achieved without them, but as we get more familiar with their capabilities, I believe we will discover a whole range of features that are enabled only through LiveBindings.
So, let me hear from you. Post a link to your example, or an example that you find on the Web, as a comment to this posting.
Monday, January 9, 2012
This post begins with a brief overview of records. It continues with a discussion of features that have been added to records since Delphi 2005. This post concludes with a look at several of the important new record types that have been introduced to Delphi in recent versions.
Record BasicsThe traditional style of record definition is somewhat similar to an array, in that it can hold more than one data value. But there are a number of features that distinguish records from arrays.
In an array the various values are identified using an ordinal index. Records, by comparison, make use of named fields. Furthermore, arrays consist of a collection of elements that are all of the same data type (even if all elements of the array are variants). In records, the individual fields can be almost any valid Delphi type, including integers, Booleans, real numbers, object references, arrays, interfaces, and enumerations.
Declaring a RecordConsider the following type declarations. Together, these types define a record type, TDog, that includes four fields: A double, an enumeration, a TDateTime, and a TObject reference.
type TPerson = class(TObject) private FName: String; public property Name: String read FName write FName; constructor Create(Name: String); end; TBreed = (Akita, Beagle, Chihuahua, Dachshund); TDog = record Weight: Double; Breed: TBreed; Born: TDatetime; Owner: TPerson; end;
Using a RecordOnce you have defined a record type, it is easy to create an instance of that record. Simply declare a variable of that record type and use it. It is not even necessary to allocate memory for the record — Delphi does that for you. For example, the following code demonstrates how to declare and use a record of type TDog.
procedure TForm1.Button2Click(Sender: TObject); var Dog: TDog; begin Dog.Name := 'Skippy'; Dog.Breed := Beagle; Dog.Born := StrToDateTime('2010-10-15'); Dog.Owner := TPerson.Create('Trevor'); ...
Actually, it's not necessary to declare a record type in order to use a record. Instead, a variable can be defined as a record in its declaration. This might look something like the following:
procedure TForm1.Button3Click(Sender: TObject); var Dog: record Name: String; Breed: TBreed; Born: TDatetime; Owner: TPerson; end; begin Dog.Name := 'Skippy'; // ...
More VariationsThere are a couple of additional variations that are somewhat unique to records, and these are associated with enumerations and variant parts. Let's consider enumerations first.
In the TDog record type, Breed was declared as a field of type TBreed. An alternative would be to declare the Breed field to be an enumeration directly, without the TBreed declaration. Here is another record type declaration that uses this syntax:
type TAppointment = record Time: TDateTime; DayOfWeek: (Mon, Tue, Wed, Thu, Fri, Sat, Sun); MeetingWith: TPerson; MeedtingBy: TPerson; end;
In the TAppointment record type, the DayOfWeek field is an enumeration, which is defined in the record type declaration directly.
Another interesting feature of traditional records is the optional variant part. A variant part is a conditional portion of the record structure that can hold variable pieces of information. The variant part, if present, always appear at the end of a record declaration.
For example, consider the following declaration of Delphi's TRect, which appears in the Types unit.
TRect = record case Integer of 0: (Left, Top, Right, Bottom: Longint); 1: (TopLeft, BottomRight: TPoint); end;
Using this syntax, you can use one or the other variant record structures (four Longints or two TPoints, though you cannot mix them in an individual instance of a record). For example, the following code declares and assigns data to two TRect instances (which are identical):
var r1, r2: TRect; begin r1.TopLeft := Point(5,5); r1.BottomRight := Point(100, 200); r2.Top := 5; r2.Left := 5; r2.Bottom := 100; r2.Right := 200;
There is a second syntax, in which the value of one of the record's fields determines which of the alternative structures the record uses. For example, consider the following record type declaration:
type TAdult = record Name: String; DateOfBirth: TDate; case MilitaryService: Boolean of True: ( WhichService: (Army, Navy, Marines, AirForce); StartDate: TDate; EndDate: TDate; ); False: ( Reason: (NoDraft, UnFit, Objection); ); end;
In this record, when the Boolean field MilitaryService is assigned the value True the WhichService, StartDate, and EndDate fields are available. If MilitaryService is False, a single enumerated field, Reason, can be used.
Class-Like RecordsBeginning with Delphi 2005, records began to receive a major overhaul with the introduction of member visibility. Until this time, all members of a record were public. Since Delphi 2005, the same visibility directives that you can use in class declarations can now also be used in record declarations.
But Delphi 2006 is the version in which the really big changes were introduced, giving records many features previously reserved for classes, and greatly improving their utility in your Delphi applications. These features include methods, properties, constructors, class constants, class variables, class static methods, class properties, nested types, overloaded operators, and record helpers. Each of these features are consider in the following sections.
Methods and PropertiesA record's methods are functions and procedures associated with the record. Like their class cousins, a record's methods can access all of the other members of the record, including fields, properties, and other methods. Similar to the methods of a class, a record's methods have a reference to the record's instance through a variable named Self.
Record methods also permit overloading. When overloading a record's method, you follow the same rules that you follow when overloading the methods of a class. Specifically, the compiler must be able to distinguish between each overloaded version based on the signature of the method (including whether the method is a function or procedure).
Record properties are also similar to their class counterparts. A record's properties can be implemented with read and/or write parts, and can use either direct access (reading and writing from fields of the record) or accessor methods (implementing reading and writing through specified methods of the record. Accessor methods permit a property to perform side effects).
The following is a simple example of a record with two fields (variables), a method, and three properties.
type TRectangle = record strict private FWidth: Double; FDepth: Double; function GetArea: Double; public property Area: Double read GetArea; property Width: Double read FWidth write FWidth; property Depth: Double read FWidth write FDepth; end;
The methods declared in a record must be implemented (records do not permit abstract methods). Furthermore, after declaring a method in a record you can use class completion (Ctrl-Shift-C) to generate the necessary implementation stubs for your record's methods. The following is the implementation of the GetArea method of the TRectangle record.
function TRectangle.GetArea: Double; begin Result := FWidth * FDepth; end;
Record ConstructorsAll records have an implicit, zero parameter constructor that is called when you attempt to use a record for the first time. If you want, you can add additional constructors. These constructors, however, must have at least one parameter, and must be distinguishable from any other overloaded constructor based on their signatures.
The following shows the TRectangle record with an overloaded constructor (note that the overload directive was not required, but makes the code clearer). The implementation of the constructor follows in this code segment.
type TRectangle = record strict private FWidth: Double; FDepth: Double; function GetArea: Double; public property Area: Double read GetArea; property Width: Double read FWidth write FWidth; property Depth: Double read FWidth write FDepth; constructor Create(Width, Depth: Double); overload; end; constructor TRectangle.Create(Width, Depth: Double); begin FWidth := Width; FDepth := Depth; end;
Class Constants and Class VariablesWhile each instance of a record has one memory location for each of its fields, class constants and class variables are shared between all instances of a given record type. A class constant is a constant value, and its declaration looks a lot like a regular declaration of a constant. The difference is that the class constant is a characteristic of the specific record type. The class constant can be read using a reference to an instance of the record type, or through the record type itself.
A class variable is a field that is shared by all instances of a particular record type. Changing the value of a class variable has the effect of changing the one memory location shared by the record type and all instances of it.
Class constants appear within a const clause of a record declaration, and class variables appear within a class var clause. These clauses conclude at the end of the record declaration, or when a method, type, variable, property, constructor, visibility directive, or variant part is encountered.
The following record type includes two class constants and two class variables:
TRectangle = record strict private FWidth: Double; FDepth: Double; function GetArea: Double; const MaxWidth = 100.0; MaxDepth = 200.0; public class var PreferredMinWidth: Double; PreferredMinDepth: Double; property Area: Double read GetArea; property Width: Double read FWidth write FWidth; property Depth: Double read FWidth write FDepth; constructor Create(Width, Depth: Double); overload; end;
Class Static Methods and Class PropertiesUnlike classes, which have both class methods and class static methods, records support only class static methods (as far as class methods go). A class static method is one that can be called on an instance of a record or through a reference to the record type itself. Like the class static methods in a class declaration, a record's class static methods have no reference to Self, and therefore cannot access any other members of the class, other than other class static methods or class variables.
Class properties are properties that can be referenced using an instance of a record type, or the record type itself. While class properties support both direct access and accessor methods (like normal properties), direct access can only employ class variables, and accessor methods must use only class static methods.
The following is a portion of a record type declaration that includes two class variables, a class static method, and two class properties. The implementation of the class static method follows.
type TRectangle = record strict private //... class var FPreferredMinWidth: Double; FPreferredMinDepth: Double; //... public class function PreferredMinArea: Double; static; class property PreferredMinWidth: Double read FPreferredMinWidth write FPreferredMinWidth; class property PreferredMinDepth: Double read FPreferredMinDepth write FPreferredMinDepth; //... end; class function TRectangle.PreferredMinArea: Double; begin Result := FPreferredMinWidth * FPreferredMinDepth; end;
Nested TypesA nested type in a record is identical to a nested type in a class declaration. A nested type is a type declaration internal to a record. The nested type is often a class type that is designed to act as a helper class. Specifically, it is designed to perform tasks related to the record in which it is declared.
The following is another record class segment, which includes a simple nested type declaration.
type TRectangle = record strict private FWidth: Double; FDepth: Double; public //... function GetArea: Double; type WorkerClass = class(TObject) private FName: String; public function SayMyName: String; property Name: String read FName write FName; end; property Area: Double read GetArea; property Width: Double read FWidth write FWidth; property Depth: Double read FWidth write FDepth; end;
Operator OverloadingOne additional feature added to records that is not shared by classes is operator overloading. In short, you can explicitly define what happens when an operator is applied to a particular record (unary operators) are to two records (binary operators). For example, you can define what happens when two of your records are added together using the addition (+) operator, or what happens if you implicitly cast your record to another type.
There are almost 30 operators that can be overloaded. These operators are shown in Listing 1.
|Method Name||Operator||Use Example||Returns|
|Negative||-||- r||return type|
|Positive||+||+ r||return type|
|LogicalNot||not||not r||return type|
|In||in||in [a, b, r]||Boolean|
|Equal||=||a = r||Boolean|
|NotEqual||<>||a <> r||Boolean|
|GreaterThan||>||a > r||Boolean|
|GreaterThanOrEqual||>=||a <= r||Boolean|
|LessThan||<||a < r||Boolean|
|LessThanOrEqual||<=||a <= r||Boolean|
|Add||+||a + r||return type|
|Subtract||-||a - r||return type|
|Multiply||*||a * r||return type|
|Divide||/||a / r||return type|
|IntDivide||div||a div r||return type|
|Modulus||mod||a mod r||return type|
|LeftShift||shl||a shl r||return type|
|RightShift||shr||a shr r||return type|
|LogicalAnd||and||a and r||return type|
|LogicalOr||or||a or r||return type|
|LogicalXor||xor||a xor r||return type|
|BitwiseAnd||and||a and r||return type|
|BitwiseOr||or||a or r||return type|
|BitwiseXor||xor||a xor r||return type|
Listing 1 Record operators that can be overloaded
To overload one of these operators, you implement the specified method name using the class operator syntax (instead of function) in your record type definition. You must also define the type of value that the operator will return. For those operators that are declared to return a Boolean value, the operator must return a Boolean value. All other operators can return whatever type is appropriate for that operator. In other words, a non-Boolean operator can return a value other than the record type.
The following code segment includes a portion of the TRectangle class with an overloaded addition operator, the implementation of this operator overload, and an event handle that uses the operator.
type TRectangle = record strict private FWidth: Double; //... public property Width: Double read FWidth write FWidth; //... class operator Add(a, b: TRectangle): TRectangle; end; class operator TRectangle.Add(a, b: TRectangle): TRectangle; begin Result.Width := a.Width + b.Width; end; procedure TForm1.Button7Click(Sender: TObject); var a, b, c: TRectangle; begin a.Width := 5; b.Width := 20; c := a + b; end;
Record HelpersLike classes in Delphi 2005, records can also have helpers. Record helpers serve essentially the same purpose as class helpers, though they only apply to records. Specifically, record helpers permit you to attach additional methods, properties, and class variables to an existing record. Record helpers can also replace existing methods of a class, if they use the same name and signature, since methods of a record helper supercede those of the record they are helping.
While the use of class helpers is largely discouraged, record helpers can play a role that cannot be provided through any other means. Specifically, record helpers provide the only mechanism for extending an existing record type. Classes, on the other hand, support inheritance. As a result, it is often better to descend a new class from an existing class, and introduce new methods and properties in the descendant than to use a class helper.
Record helpers have the same limitations as class helpers. A given record can be supported by only one record helper. If there is more than one record helper for a given record, the record will employ the record helper that is closer in scope.
The following code shows the declaration of a record helper for the TRectangle record, along with the implementation of the single method declared within the record helper. Notice in the GetMaxArea implementation that Self refers to the record instance.
type TRectHelper = record helper for TRectangle function GetMaxArea: Double; end; function TRectHelper.GetMaxArea: Double; begin Result := Self.MaxWidth * Self.MaxDepth; end; procedure TForm1.Button8Click(Sender: TObject); var a: TRectangle; begin ShowMessage(FloatToStr(a.GetMaxArea)); end;
Records Versus ClassesAt this point you might be inclined to think that the enhanced record syntax makes records nearly identical to classes, making it difficult to choose between them. The truth is that there are very significant differences between records and classes, and these differences can help you decide which to use in a particular situation.
First of all, records are allocated on the stack, as long as they are not declared globally or managed using New and Dispose. Class instances, by comparison, are allocated on the heap.
This has a significant impact on memory management. Specifically, while classes that do not have an owner must be explicitly freed, memory allocated for record instances is automatically freed when the record goes out of scope merely by its data being removed from the stack. Of course, if your record creates class instances during its lifecycle, that memory must be managed by you or your record, or a memory leak may exist. This is made a bit more difficult since records do not support destructors.
Another significant difference between classes and records is that you do not need to call a record's constructor, while a class instance must be explicitly created. You can declare a variable of a record type and then immediately use it. In this case, Delphi will call the zero parameter constructor for you.
You are still free to call a record's constructor if you want. In fact, if you've declared one or more additional constructors, it is probably because you want to create your record in a particular fashion, and you can do so by calling one of those overloaded constructors.
Another major difference between records and classes is that records do not support inheritance. A record never descends from an existing record. Similarly, records cannot implement interfaces (though they may have fields or properties of interface type, but that is different). The bottom line here is that records do not support polymorphism. A corollary of this is that record methods are necessarily static. Records do not support virtual, dynamic, or abstract methods.
As you learned earlier, records can also include a variant part. A variant part permits a record to hold data that is specific to its nature, and may vary from record instance to record instance. All instances of a class have the same set of members, regardless of their data.
Finally, records are value types, as opposed to reference types. Consider the following code:
var a, b: TRectangle; begin a.Width := 5; a.Depth := 5; b := a;
The TRectangle named b in this code segment is a completely new record that contains a copy of the data held within record a. By comparison, if TRectangle was a class type, following the assignment of variable a to variable b, variable b would point to the same memory address as a.
Finally, records support operator overloading, while classes do not. Operator overloading provides you with a rich tool for managing exactly what happens when records are used in expressions, and the results can get very interesting.
Examples of Useful Records in DelphiRecord types are playing an increasingly important role in the latest versions of Delphi, and this is due in large part to the differences between record types and class types described in the preceding section. Let's consider a few of the more significant record types.
Delphi 2009 saw the introduction of the System.TMonitor type. TMonitor is a record that can be used to acquire and manage a lock on any TObject. It provides a convenient and powerful synchronization mechanism for multithreaded applications.
A major new layer of runtime type information (RTTI) was introduced in Delphi 2010. The principle type for working with the new RTTI, TRTTIContext, is a record type.
Additional records types introduced in Delphi 2010, and further enhanced in Delphi XE, include TFile, TPath, and TDirectory. These types, which are declared in the IOUtils unit, are record types.
SummaryIt is no longer safe to assume that the entity you are calling a method on, or are reading a property from, is an object. It might be a type of record. Indeed, records types posses a unique blend of features that make them better suited for a variety of situations in which you would have previously used a class type.
Authors Note: I originally published this article in the SDN Magazine (issue 110), published by the Software Development Network on August 26, 2011. For information on the Software Development Network, visit http://www.sdn.nl/.