Tuesday, June 8, 2010

Creating Editor Key Bindings in Delphi

There is a powerful but little known feature of the code editor in Delphi and that permits you to add your own custom keystrokes. This feature is referred to as custom key bindings and it is part of the Delphi open tools API (OTA). The open tools API provides you with a collection of classes and interfaces that you can use to write your own extensions to the IDE.
This article provides you with an overview of this interesting feature, and demonstrates a simple key binding class that you can use as a starting point for creating your own custom key bindings. This key binding makes a duplicate, or copy, of the current line in the code editor. If a block of text is selected, this key binding will duplicate that block. This is a feature that is found in other code editors, and now, through key bindings, you can have it in Delphi.

Overview of Key Bindings

A key binding is a unit that installed into a design-time package. Writing an editor key binding involves creating class type declarations and implementing interfaces. In fact, creating and installing an editor key binding involves a number of explicit steps. These are:
  1. Descend a new class from TNotifierObject. This class must be declared to implement the IOTAKeyboardBinding interface. This class is your key binding.
  2. In addition to the four methods of that IOTAKeyboardBinding interface that you must implement in your key binding class, add one additional method for each new feature that you want to add to the editor. This method is passed an object that implements the IOTAKeyContext interface. You use this object within your implementation to read information about, and control, the behavior of the editor.
  3. Declare and implement a standalone Register procedure. Within this procedure invoke the AddKeyboardBinding method of the BorlandIDEServices object, passing an instance of the class you declared in step 1 as the only argument.
  4. Add the unit that includes this Register procedure to a designtime only package.
  5. Add the designide.dcp package to this design time package's Requires clause. This package is located in lib folder under where you installed Delphi.
Each of these steps is discussed in the following sections. As mentioned earlier, these steps will define a new key binding that adds a single key combination. Once implemented and installed, this key combination will permit you to duplicate the current line in the editor by pressing Ctrl-Shift-D.
(The source code for this editor key binding project can be downloaded from Embarcadero Code Central at
http://cc.embarcadero.com/item/27635.)

Declaring the Key Binding Class

The class that defines your key binding must descend from TNotifierObject and implement the IOTAKeyboardBinding interface. If you are familiar with interfaces, you will recall that when a class is declared to implement an interface, it must declare and implement all of the methods of that interface. Consider for a moment the following declaration of the IOTAKeyboardBinding interface. This declaration appears in the ToolsAPI unit:
IOTAKeyboardBinding = interface(IOTANotifier)
 ['{F8CAF8D7-D263-11D2-ABD8-00C04FB16FB3}']
 function GetBindingType: TBindingType;
 function GetDisplayName: String;
 function GetName: String;
 procedure BindKeyboard(const BindingServices:
   IOTAKeyBindingServices);
 property BindingType: TBindingType read GetBindingType;
 property DisplayName: String read GetDisplayName;
 property Name: String read GetName;
end;
As you can see, this interface declares four methods and three properties. Your key binding class must implement the methods. Note, however, that it does not need to implement the properties. (This is a regular source of confusion when it comes to interfaces, but the fact is that the properties belong to the interface, and are not required by the implementing object. Sure, you can implement the properties in the object. But you do not have to, and the properties were not implemented in this example.)
In addition to the methods of the IOTAKeyboardbinding interface, your key binding class must include one additional method for each custom feature that you want to add to the editor. In order to be compatible with the AddKeyBinding method used to bind these additional methods, these additional methods must be TKeyBindingProc type methods. The following is the declaration of the TKeyBindingProc method pointer type, as it appears in the ToolsAPI unit:
TKeyBindingProc = procedure (const Context: IOTAKeyContext;
  KeyCode: TShortcut; var BindingResult: TKeyBindingResult)
    of object;
All this type really means is that each of the additional methods that you write, each one of which adds a different keystroke to the editor, must take three parameters: an IATOKeyContext, a TShortcut, and a TKeyBindingResult.
The following is the key binding class declared in the DupLine.pas unit. This class, named TDupLineBinding, includes only one new key binding. The following is the class type declaration of this class:
type
  TDupLineBinding = class(TNotifierObject, IOTAKeyboardBinding)
  private
  public
    procedure DupLine(const Context: IOTAKeyContext;
      KeyCode: TShortCut;
      var BindingResult: TKeyBindingResult);
   { IOTAKeyboardBinding }
   function GetBindingType: TBindingType;
   function GetDisplayName: String;
    function GetName: String;
    procedure BindKeyboard(const BindingServices:
      IOTAKeyBindingServices);
  end;

Implementing the IOTAKeyboardBindings Interface

Once you have declared your key binding class, you must implement the four methods of the IOTAKeyboardBinding interface, as well as each of your additional TKeyBindingProc methods. Fortunately, implementing the IOTAKeyboardBinding interface is easy.
You implement GetBindingType by returning the type of key binding that you are creating. There are only two types of key bindings: partial and complete. A complete key binding defines all of the keystrokes of the editor, and you identify your key binding as a complete key binding by returning the value btComplete. A complete key binding is actually a full key mapping.
A partial key binding is what you use to add one or more keystrokes to the key mapping that you are using. The TDupLineBinding class is a partial key binding. The following is the implementation of GetBindingType in the TDupLineBinding class:
function TDupLineBinding.GetBindingType: TBindingType;
begin
  Result := btPartial;
end;
You implement GetDisplayName and GetName to provide the editor with text descriptions of your key binding. GetDisplayName should return an informative name that Delphi will display in the Enhancement modules list of the Key Mappings page of the Editor Options node of the Options dialog box.
GetName, on the other hand, is a unique string that the editor uses internally to identify your key binding. Because this string must be unique for all key bindings that a user might install, by convention this name should be your company name or initials followed by the name of your key binding.
The following listing contains the implementation of the GetDisplayName and GetName methods for the TDupLineBinding class:
function TDupLineBinding.GetDisplayName: String;
begin
  Result := 'Duplicate Line Binding';
end;

function TDupLineBinding.GetName: String;
begin
  Result := 'jdsi.dupline';
end;
You implement the BindKeyboard method to provide for the actual binding of your TKeyBindingProc methods. BindKeyboard is passed an object that implements the IOTAKeyBindingServices interface, and you use this reference to invoke the AddKeyBinding method.
AddKeyBinding requires at least three parameters. The first parameter is an array of TShortcut references. A TShortcut is a word type that represents either a single keystroke, or a keystroke plus a combination of one or more of the following: CTRL, ALT, or SHIFT, as well as left, right, middle, and double mouse button clicks. Because this parameter can include an array, it is possible to bind your TKeyBindingProc to two or more keystrokes or key combinations.
The Menus unit in Delphi contains a function named Shortcut that you can use to easily create your TShortcut references. This function has the following syntax:
function Shortcut(Key: Word; Shift: TShiftState): TShortcut;
In this function, the first character is the ANSI value of the keyboard character, and the second is a set of zero, one, or more TShiftState. The following is the declaration of TShiftState, as it appears in the Classes unit:
TShiftState = set of (ssShift, ssAlt, ssCtrl,
  ssLeft, ssRight, ssMiddle, ssDouble);
The second parameter of BindKeyboard is a reference to your TKeyBindingProc method that implements the behavior you want to associate with the keystroke or key combination, and the third parameter is a pointer to a context. In the BindKeyboard implementation in the TDupLineBinding class, the method DupLine is passed as the second parameter and nil is passed in this third parameter. The following is the implementation of the BindKeyboard method that appears in the DupLine.pas unit:
procedure TDupLineBinding.BindKeyboard(
  const BindingServices: IOTAKeyBindingServices);
begin
  BindingServices.AddKeyBinding(
    [ShortCut(Ord('D'), [ssCtrl, ssShift])], DupLine, nil);
end;
As you can see, this BindKeyboard implementation will associate the code implemented in the DupLine method with the CTRL-Shift-D keystroke combination.

Authors Note: In the original posting, ssShift was omitted from the set in the second argument of the above BindKeyboard implementation. A reader pointed this out in a reply to this blog, and I have since updated this code. Thanks Atys!

Implementing TKeyBindingProc Methods

Implementing the methods of IOTAKeyboardBindings is pretty straightforward. Implementing your TKeyBindingProc method, however, is not.
As you can see from the TKeyBindingProc method pointer type declaration shown earlier in this section, a TKeyBindingProc is passed three parameters. The first, and most important, is an object that implements the IOTAKeyContext interface. This object is your direct link to the editor, and you use its properties to control cursor position, block operations, and views. The second parameter is the TShortCut that was used to invoke your method. This is useful if you passed more than one TShortCut in the first parameter of the AddKeyBinding invocation, especially if you want the behavior to be different for different keystrokes or key combinations.
The final parameter of your TKeyBindingProc method is a TKeyBindingResult value passed by reference. You use this parameter to signal to the editor what it should do after your method exits. The following is the TKeyBindingResult declaration as it appears in the ToolsAPI unit:
TKeyBindingResult = (krUnhandled, krHandled, krNextProc);
You set the BindingResult formal parameter of your TKeyBindingProc method to krHandled if your method has successfully executed its behavior. Setting BindingResult to krHandled also has the effect of preventing any other key bindings from processing the key, as well as preventing menu items assigned to the key combination from processing it.
You set BindingResult to krUnhandled if you do not process the keystroke or key combination. If you set BindingResult to krUnhandled, the editor will permit any other key bindings assigned to the keystroke or key combination to process it, as well as any menu items associated with the key combination.
Set BindingResult to krNextProc if you have handled the key, but want to permit any other key bindings associated with the keystroke or key combination to trigger as well. Similar to setting BindingResult to krHandled, setting BindingResult to krNextProc will have the effect of preventing menu shortcuts from receiving the keystroke or key combination.
As mentioned earlier, the real trick in implementing your TKeyBindingProc method is associated with the object that implements the IOTAKeyContext interface that you receive in the Context formal parameter. Unfortunately, Embarcadero has published almost no documentation about how to do this. One of the few bits of information are the somewhat intermittent comments located in the ToolsAPI unit.
A full discussion of the properties of IOTAKeyContent is well beyond the scope of this article. That having been said, the following is the implementation of the TKeyBindingProc from the TDupLineBinding class:
procedure TDupLineBinding.Dupline(const Context: IOTAKeyContext;
  KeyCode: TShortcut; var BindingResult: TKeyBindingResult);
var
  EditPosition: IOTAEditPosition;
  EditBlock: IOTAEditBlock;
  CurrentRow: Integer;
  CurrentRowEnd: Integer;
  BlockSize: Integer;
  IsAutoIndent: Boolean;
  CodeLine: String;
begin
  EditPosition := Context.EditBuffer.EditPosition;
  EditBlock := Context.EditBuffer.EditBlock;
  //Save the current edit block and edit position
  EditBlock.Save;
  EditPosition.Save;
  try
    // Store original cursor row
    CurrentRow := EditPosition.Row;
    // Length of the selected block (0 means no block)
    BlockSize := EditBlock.Size;
    // Store AutoIndent property
    IsAutoIndent := Context.EditBuffer.BufferOptions.AutoIndent;
    // Turn off AutoIndent, if necessary
    if IsAutoIndent then
      Context.EditBuffer.BufferOptions.AutoIndent := False;
    // If no block is selected, or the selected block is a single line,
   // then duplicate just the current line
    if (BlockSize = 0) or (EditBlock.StartingRow = EditPosition.Row) or
      ((BlockSize <> 0) and ((EditBlock.StartingRow + 1) =(EditPosition.Row)) and
      (EditBlock.EndingColumn = 1)) then
    begin
      //Only a single line to duplicate
      //Move to end of current line
      EditPosition.MoveEOL;
      //Get the column position
      CurrentRowEnd := EditPosition.Column;
      //Move to beginning of current line
      EditPosition.MoveBOL;
      //Get the text of the current line, less the EOL marker
      CodeLine := EditPosition.Read(CurrentRowEnd - 1);
      //Add a line
      EditPosition.InsertText(#13);
      //Move to column 1
      EditPosition.Move(CurrentRow, 1);
      //Insert the copied line
      EditPosition.InsertText(CodeLine);
    end
    else
    begin
      // More than one line selected. Get block text
      CodeLine := Editblock.Text;
      // Move to the end of the block
      EditPosition.Move(EditBlock.EndingRow, EditBlock.EndingColumn);
      //Insert block text
      EditPosition.InsertText(CodeLine);
    end;
    // Restore AutoIndent, if necessary
    if IsAutoIndent then
      Context.EditBuffer.BufferOptions.AutoIndent := True;
    BindingResult := krHandled;
  finally
    //Move cursor to original position
    EditPosition.Restore;
    //Restore the original block (if one existed)
    EditBlock.Restore;
  end;
end;
As you can see from this code, the IOTAKeyContext implementing object passed in the first parameter is your handle to access a variety of objects that you can use to to implement your keybinding behavior. And without a doubt, it is the EditBuffer property that is most useful.
This property refers to an object that implements the IOTAEditBuffer interface. You use this object to obtain a reference to additional interface implementing objects, including IOTABufferOptions, IOTAEditBlock, IOTAEditPosition, and IOTAEditView implementing objects. These objects are available using the BufferOptions, EditBlock, EditPosition, and TopView properties of the EditBuffer property of the Context formal parameter.
You use the IOTABufferOptions object to read information about the status of the editor, including the various settings that can be configured on the General page of the Editor Properties dialog box.
The IOTAEditBlock object permits you to control blocks of code in the editor. Operations that you can perform on blocks includes copying, saving to file, growing or shrinking the block, deleting, and so on.
You use the TOTAEditPosition object to manage the insertion point, or cursor. Operations that you can perform with this object include determining the position of the insertion point, moving it, inserting single characters, pasting a copied block, and so forth.
Finally, you use the TOTAEditView object to get information about, and to a certain extent, control, the various editor windows. For example, you can use this object to determine how many units are open, scroll individual windows, make a given window active, and get, set, and goto bookmarks.
Turning our attention back to the DupLine method, this code begins by getting references to the IOTAEditPosition and IOTAEditBlock. While this step was not an essential step, it simplifies the code in this method, reducing the need for repeated references to Context.EditBuffer.EditPosition and Context.EditBuffer.EditBlock. Next, the current state of both the edit position and edit buffer are saved.
The code now saves the current row of the cursor, the size of the selected block (it will be 0 if no block is selected), and the AutoIndent setting of the code editor.
In the next step, the AutoIndent setting is turned off, if necesary. The code now determines whether a single line of code or a block of code needs to be duplicated. If a single block is being duplicated, the length of the current line is measured, the text is copied, and then the copied text is inserted into a new line. If a block is being copied, the selected text is copied, the cursor is positioned at the end of the selected block, and the copied text is inserted.
Finally, the AutoIndent setting is restored (if necessary), the BindResult formal parameter is set to krHandled, and both the edit position and edit block is restored. Restoring the edit position moves the cursor to its original position, and restoring the edit block re-selects the selected text (if a block was selected).

Declaring and Implementing the Register Procedure

In order for your key binding to be installed successfully into the editor, you must register it from an installed designtime package using a Register procedure. The Register procedure, whose name is case sensitive, must be forward declared in the interface section of the unit that will be installed into the designtime package. Furthermore, you must add an invocation of the IOTAKeyBindingServices.AddKeyboardBinding method to the implementation of this procedure, passing an instance of your key binding class as the sole parameter. You invoke this method by dynamically binding the BorlandIDEServices reference to the IOTAKeyboardServices interface, and pass as the actual parameter an invocation of your key binding object’s constructor.
The following is how the Register procedure implementation appears in the DupLine.pas unit:
procedure Register;
begin
  (BorlandIDEServices as IOTAKeyboardServices).
    AddKeyboardBinding(TDupLineBinding.Create);
end;

Installing the KeyBinding

The source code download includes a project for a design-time package, as well as the Dupline.pas unit. Use the following steps to install this keybinding in Delphi.
  1. Open the KeyBind project in Delphi.
  2. Using the project manager, right-click the Keybind project and select Install. The new keybinding should compile and install. (If it does not compile, ensure that the designide package is in the project requires clause, and that the project is a designtime only package.
The keybinding is now active. You should now be able to press Ctrl-Shift-D in a unit to create a duplicate of the current line or selected text.
I hope that this has inspired you to try to create your own key bindings. Note, however, if you create a key binding that uses a keystroke that is already in use by Delphi's editor, and you set BindResult to handled, you will have effectively overwritten the existing keystroke. In fact, Ctrl-Shift-D is current in use by Delphi to display the Declare Field refactoring dialog box. However, you can still access that feature by selected Refactor Declare Field from Delphi's main menu.
Or, you might consider changing the BindKeyboard implementation in this package to map DupLine to something else, such as Ctrl-D. Ctrl-D is mapped to the Source Formatting feature in Delphi 2010. This feature, which reformats your code, is something that you might prefer to have to intentionally select by right-clicking in the code editor and selecting Format Source.

Tuesday, June 1, 2010

Would You Like Chips * With That Q&A?

(* British for French Fries)

I don't go to McDonald's often. But last Wednesday, I did go to McDonald's, though it was for the technology, not for the food.

With the exception of last Wednesday, I'm not exactly sure when I last visited a McDonald's. I think it might have been almost four years ago, in Prague, and that was to get a cup of coffee (though I have a vague memory of my wife, Loy, having some kind of ice cream dish). So, I stand by my original assertion, that I do not go to McDonald's that often.

Many of my European friends find this hard to believe. Certainly, as an American, and one who is on the road a lot (most would say too much), certainly I must succumb to the siren song of the fast and easy food available at these locations. Actually, it's largely because I travel so much that I avoid fast food restaurants in general, if not in principle.

While I am not a skinny person, it would be disingenuous to characterized me as being overweight. And I work at that. In fact, when I am on the road, I rarely eat dinner at restaurants of any kind. Instead, I find something at a local supermarket and fix it up in my hotel room. Since I normally have a room with a microwave oven and refrigerator, at least in US hotels, there is a lot I can do. Making soup, building sandwiches, re-heating prepared meals, or even preparing raw vegetables. It's amazing, really, what you can do.

Even when I am not on the road, I rarely eat out. The fact is, I love to cook. To me, eating at a restaurant when I could be cooking at home would be my loss. I would much rather spent my money on ingredients, and maybe a nice bottle of wine, than to give it to someone else to prepare my food.

In fact, I am shocked each time I do eat out. My goodness, it's expensive (not that this is an issue). But when I think of what I could have done with that 30 bucks (or 50 bucks) I feel a loss. That same money could have bought some beautiful New Zealand green mussels, or Dungeness crab, or Maine lobster (I love seafood), or a great steak, or a little of each!

But my visit to McDonald's had nothing to do with food. It was for the the WIFI.

Last Wednesday I was in London, England, as part of the Delphi Developer Days 2010 tour that I was delivering with my friend and colleague, Marco Cantù. We had just finished the first day of our presentations, and I was preparing to go online for a live broadcast of my question and answer (Q&A) session for a web-based presentation I was doing for Embarcadero, called DataRage 2.

There was a problem, however. Well, two problems, really. The first problem was that the wireless Internet connection at our hotel had gone down. It had been up for most of the day, but now, just as I was supposed to go online, it was down.

I knew that this might be a problem. At this hotel the Internet goes down at least once a day. In most cases, we simply talk to the front desk and they reboot the router. There, problem solved.

But this is where the second problem comes in. And his name is Trevor. You see, Trevor works a shift at the front desk, and apparently he is uncomfortable with technology. Basically, if the Internet goes down when Trevor is on duty, you might as well wait until his replacement comes in, and they will have it back up in minutes.

Asking Trevor to reboot the router is like asking your plumber for health tips. It's a pointless exercise.

When I reported to Trevor that the hotel had lost its Internet connection, a hint of panic crosses his face. Next he stutters that there is nothing that he can do. When I comment that the other managers simply reboot the router, he starts flipping the power switches on and off on two power strips connected to who knows what, over and over, until he reports that, "There, it's gone. I'm sorry, it won't turn on again."

It's gone? Trevor, what have you done? I offer to come behind the counter and take a look. After all, I am a computer guy. You know, one of those people who work with these things all the time.

But Trevor would have none of this. His stuttering is getting worse. He's not supposed to touch it, he informs me. No one is supposed to touch it. It's not the hotel's equipment. No, I cannot come behind the counter. He's becoming more agitated. "We'll just have to wait for someone to come in and fix it," he informs me.

While this was going on, my presentation was already being broadcast. I submitted a 30- minute recording some weeks before, and this is played prior to my live question and answer period. However, it is now 5:22 pm (London time), and there are about 10 minutes left before I am supposed to speak.

I grab my backpack, which contains my computer, and start to head out the door. I ask Marco, who has a mobile phone, to send a message to DataRage coordinator Christine Ellis at Embarcadero and let her know that I am going to try to find an Internet café from which I can talk.

As I head towards the high street, where I had previously seen several Internet cafés, it occurs to me that I left my USB headphones, the ones with over-the-ear earphones and a noise canceling microphone, back in the hotel. But it's too late to go back. I need to get to an Internet connection as soon as possible.

A couple of minutes later I turn the corner onto the high street, and immediately find an Internet café. Actually, it was more of an all-in-one shop, international phone calls, prepaid phone cards, and several Internet-connected computers lined up along one wall. Success!

"Do you have wireless?" I ask. "No" was the simple reply. Drat! I need wireless.

My laptop is already set up with Microsoft Live Meeting, which takes several minutes to install on an existing machine. And, it's not clear that these machines even have microphones, or that I would be permitted to install Live Meeting. I needed another option.

"Is there an Internet café nearby that provides wireless access?" I ask. "Well," the clerk informs me, "if all you need is wireless access, go to the McDonald's across the street. It's free there."

I quickly thank him and leave. The McDonald's across the street was not actually across the street, but I could see it from here. And, I needed to go down to a crosswalk, as this was a very busy street.

As I entered the McDonald's, I did feel a tinge of guilt. I am not a regular customer, and I don't intend to buy anything this time, either. So, I should be as inconspicuous as possible. But as I sat down at a single table in a far corner, away from most of the rest of the patrons, I realize that this is one noisy place.

To begin with, the place is heaving (this is a particularly British phrase). It's now just after 5:30pm, and my Q&A should be starting. But so is dinner service, and the place is filled with families with young children, each one speaking at the top of their voices. On top of this was the music, a pulsing electronic pop that blared from speakers in the ceiling. The music may have been louder than the children. But the cacophony of it all was too much. There is no way that I could talk from here.

I began to put my computer back into my bag when I realize that it was going to be McDonald's or nowhere. The nearest Internet café that I could recall was at least two blocks away. I was going to have to make do. If nothing else, maybe I could type my answers to any questions asked.

So, I restart my computer, and connect to the McDonald's Internet (this took several minutes, as I had to register with their provider first). However, I was soon connected and Live Meeting was loading. During this setup time I once again realized that I didn't have my USB headphones. How am I going to hear the questions?

I scrounge around in my bag and find an extra pair of cheap earphones that I got on my flight to London. Actually, these were ear buds, and poor ones at that. But, they'd have to do.

Just about the time I have the ear buds plugged in, and Live Meeting is finally coming on line, I hear the rich voice of Embarcadero Developer Relations Evangelist David Intersimone (affectionately known as "David I") answering a question about my presentation. Suddenly, he stops and says "It sounds like Cary has come online from an Internet café. Cary, are you there?"

Oh, I'm here all right. Leaning over my laptop, speaking directly into the built-in microphone of my laptop's lid, with my fingers in my ears, trying to push the ear buds in further so that I could hear David while trying to block as much of the external racket as possible. "Yes, David, I'm here. Coming to you from a McDonald's."

"I can hear that. Sounds like you have a big audience there." But the good news was that I could hear the questions, and remarkably, they could hear my answers. And that's the way it was for the next 20 minutes. Me leaning over my laptop with the forefinger of each hand stuck in each ear.

I must have been quite a sight. But certainly not enough to discourage the family of five, an exhausted mother and her four children, all yelling at each other at the top of their lungs, from sitting down next to me half way through my session. Gee, I would have thought that a lunatic talking to his computer with his fingers in his ears would be something that a nurturing mother would want to avoid. But what do I know? This is London, after all, and they have their fare share of lunatics, and most of them are harmless.

Meanwhile, back at the conference hotel, Marco and Loy are back online and listening in (and laughing pretty hard, I'm later told). Apparently someone has rescued Trevor.

Considering the circumstances, it all went quite well. And at the conclusion of the Q&A, David I noted that we had accomplished a first. Other presenters had handled their Q&As live from the Embarcadero studios, some from their own offices, others from home, but never from a McDonald's. And, despite the constant chatter of children in the background (Marco and Loy said it sounded like I was presenting from a daycare center), we managed to answer all of the questions asked.

So, I'll have to admit that my first visit to McDonald's in years was an overall satisfying experience. Maybe I'll be back another day, when there is less pressing business. After all, I hear they pour a world-class cup of coffee these days.

Copyright (c) 2010 Cary Jensen. All Rights Reserved

Thursday, May 6, 2010

Delphi Non-Core Feature Survey: You Can Help

Delphi developers, I want to hear from you. Are you using Delphi's frameworks for unit testing, audits, metrics, and design patterns? If no, why? If yes, to what extent do you use these features? Click here to take the survey

Here is a little background. Since the release of Delphi 2005, there have been a number of interesting support features introduced in Delphi. For example, Delphi 2005 added support for easily creating unit tests. And audits, metrics, and design pattern support was added in Delphi 2006.

Initially Delphi's support for unit testing was available in all versions (Professional, Enterprise, and Architect). By comparison, the support for audits, metrics, and design patterns required Together, and this product was shipped only with the high-end versions of Delphi. And, on top of that, the Together product stayed with Borland when CodeGear was spun off. So what did that mean for these features as Delphi evolved?

I have to admit that I have not used the audits, metrics, and design pattern features of Delphi, though I have noticed the associated menu items in Delphi's menus. So, when I got a request from a client to include discussion of audits and metrics in an upcoming Delphi class that I am going to deliver, it was time to do some research.

What I found was interesting and puzzling. There is not a whole lot of information out there about these features. And, I did discover that not only are these features available in the absence of Together, but are now even included (with limited support) in the Professional sku of Delphi 2010.

I have now committed to writing training material on these topics, and this will also lead to my adapting this material for this blog as well for some magazine articles that I am writing. And this is where your help comes.

I want to hear from you. If you are a Delphi developer, I want to know which of these features you use, and to what extent. If you don't use these features as they ship in the product, do you use third-party tools that provide similar support?

I have created a short, 10 question survey that should take only a couple of minutes to complete. Please help me by filling out this survey. In addition, please ask your Delphi colleagues to help out as well. Click here to take the survey

Copyright (c) 2010 Cary Jensen. All Rights Reserved.

Wednesday, May 5, 2010

In-Memory DataSets: ClientDataSets and .NET DataTables Part 6: Applying Updates to a Database

In the preceding article in this series I discussed various techniques that you can use to manage the change cache. In this installment I will conclude that discussion by looking at how you can apply the changes held in the change cache to the underlying database from which the data was originally loaded.

Call the ApplyUpdates method of a ClientDataSet to save any changes made to the in-memory data to the underlying database. Specifically, if you edit data obtained through a dataset provider, and then close or free a client dataset without calling ApplyUpdates, any changes stored in the change log are lost.

When ApplyUpdates is called, the contents are sent back to the dataset provider for resolution in the context of a transaction (so long as you pass a non-negative integer as the sole parameter of the ApplyUpdates method. When you pass –1, no transaction is initiated). The dataset provider, in turn, generates the necessary calls to apply the updates to the underlying dataset, applying the changes to the underlying dataset one record at a time.

By default this process is handled by a SQLResolver instance, which is created by the dataset provider. The SQL resolver determines the database that needs to be updated, and then creates the necessary SQL statement to apply the changes based on the contents of the change log. Specifically, one SQL statement will be generated for each change that needs to be applied.

Alternatively, the dataset provider can be configured to use the dataset from which it originally read the data to apply the changes. This approach is only possible when the dataset from which the records were read permits data changes.

For example, if the dataset provider gets its data from a TTable, the dataset provider can edit the TTable directly, inserting, deleting, or posting the changes using the TDataSet interface. Again, these changes are applied one at a time. It should be noted that when the dataset provider resolves the changes through the dataset, the dataset's event handlers, such as BeforeDelete and BeforePost can be used to perform data validation.

By comparison, if the dataset provider gets its data from a dataset that does not permit data editing, the dataset provider cannot resolve the data to the dataset. For example, if the dataset provider gets its data from a SQLDataSet, a dataset that retrieves its data using a unidirectional cursor and which does not permit editing, the dataset provider cannot resolve the changes directly to the dataset.

In these cases, there are two options. Either the default SQLResolver described earlier can be used, or you can write a BeforeUpdateRecord event handler. From within this event handler your code is given a reference to the changes that must be applied, and your code can take any action necessarily to explicitly apply these changes. This approach is the most flexible, although the most difficult to implement.

What kind of SQL (or direct edit) is generated is controlled by the UpdateMode of the DataSetProvider. If set to upWhereAll, a record is updated only if that exact record currently exists in the database. If set to upWhereChanged, the record is updated if a record with the same key fields and same values in the modified fields are found (this is a merge). When set to upWhereKeyOnly, an update is made if a record with the same key is found (last to post wins).

Updating .NET DataSets is similar to updating ClientDataSets. In that case, however, the DbDataAdapter class is the one typically used to apply the updates. DbDataAdapters, such as DataStoreDataAdapter, have four DbCommand properties. The SelectCommand property contains the SQL statement that returns the result set that is inserted into the DataTable when you call the Fill method. The other DbCommand properties, DeleteCommand, InsertCommand, and UpdateCommand, are designed to hold parameterized queries that will get their parameter values from the change log at runtime when you call the DbDataAdapter.Update method.

Some developers write the SQL statements that define the DeleteCommand, InsertCommand, and UpdateCommand objects manually. Doing so give them control over the queries, permitting the queries to be optimized for the underlying database. Other developers use a CommandBuilder to generate these queries. CommandBuilders are easy to use, but do not always generate optimized queries.

To use a CommandBuilder, you call its constructor, passing to it the DbDataAdapter whose SelectCommand command has already been defined. Based on this query, the CommandBuilder generates the DeleteCommand, InsertCommand, and UpdateCommand queries.

The following code shows the configuration of a CommandBuilder:

Connection1 := DataStoreConnection.Create(
'host=LocalHost;user=sysdba; ' +
'password=masterkey;database="C:\Users\Public\Documents\' +
'Delphi Prism\Demos\database\databases\BlackfishSQL\employee"';
Connection1.Open();
//Sql statements are executed by IDbCommand objects
Command1 := DataStoreCommand.Create('SELECT * FROM customers',
Connection1);
//DbDataAdapters are used to populate DataTables and resolve data
DataAdapter1 := SqlDataAdapter.Create(Command1);
//CommandBuilders create DbCommand objects for a DataAdapter's
//DeleteCommand, InsertCommand, and UpdateCommand properties based
//on the DbDataAdapter.SelectCommand IDbCommand property
CommandBuilder1 := DataStoreCommandBuilder.Create(DataAdapter1);
DataSet1 := DataSet.Create;
DataAdapter1.Fill(DataSet1);

The following code shows how the changes made to the DataTable are applied.

var
DataTable1: DataTable;
begin
DataTable1 := DataSet1.Tables[0].GetChanges;
if DataTable1 <> nil then
DataAdapter1.Update(DataSet1.Tables[0]);

Applying Updates and Persisted Data

Probably one of the most important characteristics of an in-memory dataset's ability to apply its updates to the underlying database is its concurrent ability to persist its data and state to a file, stream, or database. Together, these features permit an in-memory dataset to apply its updates at some future time, regardless of whether it has been persisted or not (and independent of its duration of persistence).

In order for a dataset to be able to apply its updates to a database subsequent to its persistence, the dataset's change log must be intact. Without this information, you lack the data required to determine which changes have been made to the dataset since it was originally populated.

If you are relying on a DataSetProvider (for ClientDataSets) or an DbDataAdapter implementation (for .NET) to apply the updates to the database, the object must be in a state compatible with applying the dataset's updates. For example, a DataSetProvider that you use to apply a previously persisted ClientDataSet's updates to a database must point to a TDataSet whose structure is consistent with the one that was used to originally load the ClientDataSet (unless you are using the DataSetAdapter's BeforeUpdateRecord event handler to programmatically apply the update, in which case it's all up to your code).

In the case of a .NET DataTable, the DbDataAdapter that is used to apply its updates must hold DbCommand instances in its DeleteCommand, InsertCommand, and UpdateCommand properties that contain parameterized queries sufficient to the task of applying those updates. This can be achieved by defining these queries manually, or by having an adequate DbDataAdapter.SelectCommand instance that can be used by a CommandBuilder to construct the necessary queries (of course, you must then use a CommandBuilder to create those delete, insert, and update queries objects). Otherwise, you once again must take matters into your own hands and generate all calls to update the underlying database by programmatically examining the change log and generating the necessary queries.

What If Updates Cannot Be Applied?

There is another issue that is worth mentioning. Specifically, even when you can construct the necessary queries to apply updates contained in a persisted in-memory dataset, those updates may not be possible. Specifically, it is conceivable that between the time that the data was originally loaded into the in-memory dataset and the point in time at which you want to write it back to the underlying database, the corresponding records in the database have been changed.

While many developers worry about this possibility, it is normally less of a concern than it might at first appear. For example, if you delete a record from an in-memory dataset, and then attempt to apply that deletion (at a later time), but find that the record was already deleted by someone else, who cares? It's gone. Mission accomplished (by someone, at least).

Similarly, if you attempt to apply a record insertion, only to find a record with that key already inserted, then the insertion is not necessary. But what, you might ask, if another user inserted a record with the same key as you (which causes your insertion to fail), but the other user's inserted record is different than the one you intended?

This is really an architectural issue, isn't it? Many of today's developers avoid this issue altogether by assigning arbitrary (and more or less meaningless) primary keys to each and every record. For example, many developers use GUIDs (globally unique identifiers ¾ 128-bit values that are guaranteed to be unique), to each record. In these cases, duplicate records are impossible, no matter who inserts the record.

Updates are a bit more problematic, but nonetheless generally workable. In most cases it is once again an architectural issue. There are, in fact, three options. In the first case, an attempt to update a record that you are holding in memory fails if someone else has changed any part of that record (in the underlying database) since the time you originally loaded it into memory. In this situation, your update query should include a test for all original field values in the WHERE clause of the update query.

In reality, failure to update a record that has been subsequently updated by another user, when those updates must be entirely exclusive, is rarely a tragedy.

The second case is to permit two or more users to update a record, so long as neither user changes the key field values, or a common field. This is a fine compromise in many situations. For example, if, since the time that you read your record into memory and the time that you are attempting to apply your update, another user has changed that record, but not one of the fields that your user changed, who cares? Simply include the record's key fields, and your changed fields, in the WHERE clause of the update query, ignoring those non-key fields you did not change. (If you use an arbitrary key field, such as a GUID, it is unreasonable, and should be prohibited, that anyone should ever change the key field value).

The third option, and one that is appropriate for some database applications, is simply that the last user to attempt to write to a record be permitted to do so, overwriting any previous user's applied updates. For example, imagine that five different sensors record the current temperature in a given area. If all sensors are equally correct, the most current update attempt is the most accurate. Who cares about a temperature that was written ten minutes ago? A similar analogy can be made with respect to stock prices. A more current stock price is more accurate. Past updates are old news.

Copyright (c) 2010 Cary Jensen. All Rights Reserved.

Sunday, April 25, 2010

In-Memory DataSets: ClientDataSets and .NET DataTables Part 5: Managing the Change Cache

The change cache is the source of much of an in-memory dataset's power. It is through the change cache that you can permit a user to review their changes before committing them, as well as permit you to programmatically work with changes that have been made to the dataset.

Note: In the first article in this series, I called the change cache the change log. A reader responded that what I described was not really a log, since it doesn’t keep track of individual changes, only information about the state of individual records. The term change log originates from the TClientDataSet interface, which includes the LogChanges property and the MergeChangeLog method. I agree that the term change cache more accurately reflects this feature of in-memory datasets, and will use that term from now on.

The following are operations that can be performed on the change cache for any in-memory dataset:

  • Detecting changes
  • Cancel all changes
  • Filter on changes
  • Detect field-level changes
  • Cancel a single change
  • Erase the change cache
  • Commit changes in the change cache to the underlying database

The first six of these operations are described in the following sections. Committing changes to an underlying database is a much larger topic, and I will cover it in the next installment of this series.

Detecting Changes

You use the ClientDataSet's ChangeCount property to determine if there have been any changes to the data in the ClientDataSet since if was originally loaded. There are no changes if ChangeCount returns 0 (zero). If there have been one or more changes, ChangeCount returns the total number of records that have been inserted, deleted, or modified (including changes to nested datasets).

Note that ChangeCount only counts changed records. For example, a record that is inserted and then modified counts as only one change.

The following code segment demonstrates the use of ChangeCount.

if ClientDataSet1.State in [dsEdit, dsInsert] then
ClientDataSet1.Post;
if ClientDataSet1.ChangeCount > 0 then
ClientDataSet1.ApplyUpdates(0);

Note that there is a bug in ClientDataSets in the original release with Delphi 6 and 7 with respect to ChangeCount. If you save a ClientDataSet to a file or stream, and then later restore the ClientDataSet, ChangeCount will be zero immediately after you reload the ClientDataSet, even if there are changes in the change cache.

In .NET, you call the DataTable's GetChanges method. If GetChanges returns a nil reference, there are no changes. If there are changes, the DataTable returned by GetChanges contains the inserted, deleted, and modified records.

if DataTable1.GetChanges <> nil then
begin
//Work with the changes here
end;

Canceling All Changes

When you cancel all changes, the in-memory dataset reverts to the values it contained when it was originally loaded. You cancel all changes in a ClientDataSet by calling the CancelUpdates method. With a .NET DataTable, you call RejectChanges.

Canceling all changes is an irreversible action.

Filtering On Changes

Both ClientDataSets and .NET DataTables permit you to filter the dataset to display only inserted, deleted, and modified records. With a ClientDataSet, you invoke this operation using the StatusFilter property, which can be set to include the following four flags: usModified, usInserted, usDeleted, and usUnModified.

The following code segment demonstrates the use of StatusFilter:

if ClientDataSet1.State in [dsEdit, dsInsert] then
ClientDataSet1.Post;
if ClientDataSet1.ChangeCount > 0 then
begin
ClientDataSet1.StatusFilter := [usModified, usInserted, usDeleted];
//Do something with the changed records
end;

Just as you must use a DataView to obtain a filtered view of a DataTable, you must use a DataView to filter on Changed records. In this case, you use the RowStateFilter and set it to one of the following eight values of the DataViewRowState enumeration: OriginalRows, CurrentRows, Added, Deleted, ModifiedOriginal, ModifiedCurrent, UnChanged, and None.

The following code demonstrates how to obtain a DataView that contains only the records deleted from a DataTable:

DataView1 := DataView.Create(DataTable1);
DataView1.RowStateFilter := DataViewRowState.Deleted;
if DataView1.Count > 0 then
begin
// Do something with the deleted records
end;

Detecting Field-Level Changes

With ClientDataSets, you detect field-level changes by examining the OldValue and NewValue variant properties of your ClientDataSet's TFields. The following code demonstrates how to do this:

if ClientDataSet1.UpdateStatus = usModified then
begin
if ClientDataSet1.Fields[0].OldValue <> ClientDataSet1.Fields[0].NewValue then
begin
//The first field in the current record has been changed
end;
end;

With .NET DataTables, you must obtain two DataViews for the table, setting the RowStateFilter on one of them to ModifiedOriginal, and the RowStateFilter of the second to ModifiedCurrent. You can them compare the fields of the two DataViews to determine what has changed. This is demonstrated in the following code sample:

DataView1 := DataView.Create(DataTable1);
DataView2 := DataView.Create(DataTable2);
DataView1.RowStateFilter := DataViewRowState.ModifiedOriginal;
DataView2.RowStateFilter := DataViewRowState.ModifiedCurrent;
if DataView1.Item[0].Row[0].ToString <> DataView0.Item[0].Row[0].ToString then
begin
//Field 1 of record 1 has been changed.
//Assumes a string-compatible field
end;

Canceling a Single Change

ClientDataSets provides you with two mechanisms for canceling a change to a record. You call the UndoLastChange method to revert the record that was last inserted, deleted, or modified to its original state. By comparison, if the ClientDataSet is pointing to a record that was inserted, deleted, or modified, you can call RevertRecord.

The following code demonstrates the use of RevertRecord:

if ClientDataSet1.UpdateStatus in [usInserted, usDeleted, usModified] then
ClientDataSet1.RevertRecord;

For a .NET DataRow, you call the RejectChanges method to restore an inserted, deleted, or modified DataRow to its original state. The following code demonstrates how to restore all deleted records to a DataTable:


var
DataView2: DataView;
i: Integer;
begin
DataView2 := DataView.Create(DataSet1.Tables[0]);
DataView2.RowStateFilter := DataViewRowState.Deleted;
for i := (DataView2.Count -1) downto 0 do
DataView2.item[i].Row.RejectChanges;

Erasing the Change Cache

Erasing the change cache leaves any changes to the dataset intact, but deletes the record of those changes. Most developers do not want to erase the change cache since it makes it impossible to undo changes and also erases the information necessary to write the changes to the underlying database. About the only time that erasing the change cache makes sense is when you want to make the changes permanent before storing the dataset in a file or stream, and never have to resolve those changes back to some underlying database.

With ClientDataSets, you erase the change cache by calling MergeChangeLog. (Note that you can simply turn off the change cache with a ClientDataSet by setting the ClientDataSet's LogChanges property to a Boolean False before making any changes.)

With .NET DataTables, call AcceptChanges to erase the change cache.

Copyright © 2010 Cary Jensen. All Rights Reserved.

Thursday, April 15, 2010

Delphi Developer Days 2010

It’s less than a month away from the first city in the 2010 Delphi Developer Days tour, and I thought this might be a good time to share some history and insight into this event. To begin with, Delphi Developer Days is an intense, two-day Delphi event featuring me and Marco Cantù. Our first city on this tour is Baltimore/Washington DC (May 11-12), continuing on to Chicago (May 14-15), Los Angeles (May 17-18), London, England (May 26-27); and finishing up in Frankfurt, Germany (May 30 – June 1).

Marco and I have known each other for a long time, are good friends, and have been looking for an opportunity to work together. In May of 2008 we talked about the lack of a live Delphi event in the US, and thought that this provided us an opportunity to offer something unique.

We weren’t interested in trying to run a conference similar to the past annual Borland Conferences or DelphiLive!. Instead, we were looking to offer something similar to a traditional training, but with the interaction and networking that a somewhat larger event can provide. As a result, we limit attendance to around 30 attendees in each city. This gives us a chance to interact with each attendee, while maintaining a critical mass for networking. (In addition to our consultant and development services, we’re both seasoned Delphi trainers.)

But there were a number of challenges that we knew we’d have to address in order for the event to be successful. To begin with, Delphi is a mature product. While there are always some newcomers, most Delphi developers have been using it for some time. Furthermore, not everyone is using the latest versions of Delphi. In fact, by some estimates, nearly half of all active Delphi developers are using Delphi 7 or earlier.

Another challenge is that there are a lot of different types of Delphi developers. Many are engaged in traditional client/server database development, while others are building server-side applications that operate over the Internet. How do you offer something that will appeal to such a diverse group of developers?

Honestly, I think the solution we came up with is the right one. To begin with, we embrace the richness of the Delphi community. We include timely topics about the latest versions of Delphi. Even if you are working with an older version, these topics are relevant. Specifically, even if you are not ready to upgrade now, you want to be informed about what the latest versions can do for you and your development efforts. In the long run, unless your applications are all at the end of their lifecycle, you will upgrade sooner or later.

At the same time, we realize that there is always room for fundamentals, topics that apply to many versions of Delphi (and some of these go all the way back to Delphi 1!). As far as types of applications, we understand that we need to have a healthy dose of both Windows client development as well as distributed computing.

Finally, we wanted to deliver this type of content in a way that would be fun for us as well as our attendees, and do so in a way that gives the attendees something of value, something that lasts well beyond the two days we spend together.

All of these considerations went into the format of Delphi Developer Days. There are a total of 12 presentations over the course of two days, four of which Marco and I do together, and eight which we do individually (four sessions where we each present simultaneously in separate rooms). In choosing the topics, we tried to include something for everybody, from “What’s New in Delphi and Delphi Prism,” to “Leveraging ClientDataSets.” From “Internet Delphi Application Technologies Compared,” to “Delphi Development for Windows 7.” We even include a foundation presentation on “Designing Interfaces and Objects.” (For a complete schedule, visit http://www.delphideveloperdays.com/descriptions.html.)

When Marco and I are presenting separately, attendees can attend either talk. But what if you are interested in both topics? While we tried to keep the simultaneous individual session topics from overlapping, we acknowledge that this is an issue. Fortunately, both Marco and I are authors, and have published close to 40 books between us. To put this another way, we know how to write, and like doing it.

Each attendee to Delphi Developer Days receives a course book written by us that includes a paper for each talk. So, if you choose to attend one individual session, you can read the paper for the other session that is going on at the same time. Last year our course book was 500 pages in length (you get our slideshows and code, too, but these papers are detailed papers, covering more information than we have time for in our live sessions). We are currently writing the course book for this year’s event, and I’m guessing that it will be similar in length.

This is a course book that you can’t get anywhere else. While there is occasional overlap with other writings that we’ve done (such as our blogs, books, magazine articles, or other courseware), we do not publish or otherwise make this material available outside of Delphi Developer Days. Sorry, please don’t ask to buy the course book; it’s for attendees only.

As I mentioned above, this is not the first time Marco and I have done this. Delphi Developer Days 2009 was a wonderful success. Marco and I had a blast, and the feedback we received encouraged us to do it again this year, with two additional cities added to the tour.

So how does this year’s tour compare to last year’s? In addition to adding two cities (Los Angeles and Frankfurt, Germany, cities added in response to requests from the Delphi community), we added new topics and brought back some of the popular ones. And in order to make this year’s event relevant for previous attendees, we made sure that when a previous topics was covered during our breakout sessions, it was paired with a new topic, giving previous attendees a choice of something new. And, judging from the number of last year’s attendees who have registered for this year, we succeeded.

I would be remiss if I didn’t also mention our sponsors. Embarcadero Technologies and Sybase are once again our platinum sponsors, and each city will include a short presentation by representatives of one or both of these companies. In addition, we have more than 20 additional sponsors from almost every segment of the Delphi community. Many of these sponsors offer tools that work with Delphi, and many of them have provided copies of their products as door prizes for attendees. These sponsors are among the most active companies in the Delphi community, and we encourage you to visit their sites and learn more about their products. You will find a list of our current sponsors at http://www.delphideveloperdays.com/sponsors.html.

But there is more. On the first night of each US city, either Anders Ohlsson or David I (David Intersimone) will be present to provide a two hour “Embarcadero Delphi Evening,” complete with loads of information about upcoming plans. (Anders will be in Baltimore/Washington DC and Chicago, David I in Los Angeles.) Even if you can’t attend Delphi Developer Days, you can come to these free events. Visit http://edn.embarcadero.com/article/40546 to register (Delphi Developer Days attendees do not need to register. Your registration for Delphi Developer Days takes care of that).

I want to finish up by saying that Marco and I are huge Delphi fans, and we are extremely positive about the future of Delphi and the Delphi Community as a whole. We hope to see you there. It’s going to be a blast.

Learn more at http://www.delphideveloperdays.com/

Copyright © 2010 Cary Jensen. All Rights Reserved.

Saturday, March 20, 2010

In-Memory DataSets: ClientDataSets and .NET DataTables Part 4: Navigating, Sorting, and Filtering

In the preceding article in this series I discussed how to populate in-memory datasets. In this continuation, I will look at some of the ways that you can programmatically work with that data, including navigating, sorting, and filtering the data in memory.

Active ClientDataSets are essentially a cursor pointer that refers to the current record in the dataset. To navigate which record your ClientDataSet refers to, you use methods of the ClientDataSet to change the current record. These methods include First, Next, Prior, Last, and MoveBy, to name a few.

The following line of code demonstrates how to navigate to each record of (scan) a ClientDataSet:

ClientDataSet1.Open;

while not ClientDataSet1.EOF do

begin

//Do something here with the current record

ClientDataSet1.Next;

end;

DataTables in the .NET framework do not support the concept of a current record. Instead, you use the Rows property to indicate which DataRow of the DataTable you want to work with.

The following code segment demonstrates how to programmatically navigate the DataRows of a DataTable.

var
i: Integer;
DataRow1: DataRow;
begin
for i := 0 to DataTable1.Rows.Count – 1 do
begin
DataRow1 := DataTable1.Rows[i];
//Do something with the DataRow
end;

While the DataTable lacks the concept of a current record, Windows Forms applications have a concept that is very similar to current record. Anytime one or more properties are bound to a multivalue binding context (such as a DataTable) in a Windows Forms application, a CurrencyManager is created that can be used to determine to which DataRow the form's controls are synchronized. If you bind data using a BindingSource instance, CurrencyManager property points to the underlying CurrencyManager. Similarly, there are classes, such as BindingSource, that provide features similar to a CurrencyManager.

Note that in .NET Web Forms applications, most multi-value controls automatically iterate through the records of a DataTable or DbDataReader implementing instance when you call the DataBind method. As a result, Web forms do not have currency managers.

Sorting In-Memory Data

ClientDataSets are sorted using ClientDataSet indexes. There are two types of indexes in ClientDataSets, temporary indexes and persistent indexes.

You create a temporary index using the IndexFieldNames property of the ClientDataSet. When you assign a field name (or a comma-separated list of field names) to the IndexFieldNames property, an index is create for that field (or fields) at that moment the property is assigned, and the records of the ClientDataSet are then sorted based on that ascending index. This is demonstrated in the following code segment:

ClientDataSet1.IndexFieldNames := 'State;City';

Persistent indexes, on the other hand, are defined using the IndexDefs property of the ClientDataSet. After defining one or more IndexDef instances, you then set the IndexName property of the ClientDataSet to the name of one of those IndexDef instances.

Indexes created using IndexDefs are more flexible than temporary indexes. While temporary indexes can only be ascending, case-sensitive indexes, your IndexDef definitions can define ascending or descending indexes, as well as case insensitive indexes.

The following line of code demonstrates the creation and use of a persistent index using an IndexDef definition:

ClientDataSet1.IndexDefs.Add('IDIndex', 'State;City',[ixCaseInsensitive]);
ClientDataSet1.IndexName := 'IDIndex';

DataTables cannot be sorted directly. If you want to sort the records in a DataTable, you use a DataView. You sort a DataView using its Sort property, which can hold either the name of a DataColumn to sort the DataView DataRows, or a comma-separated list of DataColumn names.

The following code demonstrates creating a DataView to display a sorted list of a DataTable's DataRows.

var DataView1: DataView;
DataView1.Create(DataTable1);
DataView1.Sort := 'Customer ID';

Rather than creating a new DataView to display sorted DataRows, you can use the DefaultView of a DataTable to refer to the DataView that each DataTable automatically supports. This is shown in the following code segment:

DataTable1.DefaultView.Sort := 'Customer ID';

The alternative is to create a new DataView instance for each sort order you want to use. The following example creates two DataViews for a single DataTable, each with its own sort order:

var
DataView1, DataView2: DataView;
begin
DataView1 := DataView.Create(DataTable1);
DataView1.Sort := 'Customer ID';
DataView2 := DataView.Create(DataTable1);
DataView2.Sort := 'Country;City';

Filtering In-Memory Data

When you filter a ClientDataSet, it behaves as though it contains only those records that meet the filter criteria. For example, if you filter a ClientDataSet for all records where the City field contains the name 'London', that ClientDataSet will act as though it only contains records where the City field value is London.

Note: There is one exception to the behavior of filtered ClientDataSets. Specifically, if you read the XMLData property of a ClientDataSet or call SaveToFile or SaveToStream, the results are as if the ClientDataSet is not filtered. In other words, all records are involved, not just the filtered records. To save only the filtered records, you must point to the filtered ClientDataSet with the DataSet property of a DataSetProvider that is referenced by another ClientDataSet. After opening that other ClientDataSet, it will contain only the filtered records (a copy), which you can then save to a file or stream. These records, however, do not include a change log.

There are two ways to filter a ClientDataSet. One is to use the Filter and Filtered properties. Filter takes a Boolean expression. Records that are available in the filtered view are those for whom the expression evaluates to True. The following code segment demonstrates how to filter a ClientDataSet using the Filter and Filtered properties.

ClientDataSet1.Filter := '[City] = ' + QuotedStr('London');
ClientDataSet1.Filtered := True;

The second way to filter a ClientDataSet is to use the SetRange method. Unlike Filter and Filtered, SetRange requires that you first set an index. Since an index is being used, SetRange is a much faster filter.

The following code demonstrates the use of SetRange.

ClientDataSet1.IndexFieldNames := 'City';
ClientDataSet1.SetRange(['London'],['London']);

You cancel filtering with Filter and Filtered by either setting Filtered to False or setting Filter to an empty string. You cancel filtering using SetRange by calling CancelRange. (For an interesting use of ClientDataSet where Filtered is False but a non-empty value appears in the Filter property, see the FindFirst, FindLast, FindNext, and FindPrior methods).

As is the case with sorting, you do not actually filter a DataTable. Instead, you use a DataView, which supports filtering through its RowFilter property. Filters are more complicated for Delphi developers, since single quotes are used to delimit string literals in the Filter expression. For example, consider the following filter:

DataView1.RowFilter := '[City] = ''London''';

With Delphi Prism, you can use the double quote character, which makes creating filter strings much easier, as shown in the following code.

DataView1.RowFilter := "[City] = 'London'";

You remove a previously set filter on a DataView by setting its RowFilter property to an empty string.

While it’s true that filtering a DataTable requires the use of a DataView, it is not necessary to actually create a new DataView when you want to set a filter. Each DataTable has a default DataView, which controls how the records in the DataTable appear. This DataView is accessed through the DefaultView property of the DataTable. As a result, assuming that the preceding code sample used a DataView created for a DataTable named DataTable1, the following code segment produces the same result:

DataTable1.DefaultView.RowFilter := "[City] = 'London'";

Copyright © 2009-2010 Cary Jensen. All Rights Reserved