Monday, August 30, 2010

Breakpoints with Side Effects

A breakpoint with a side effect is a breakpoint that causes a task to be performed upon encountering the enabled breakpoint within your application. Of course, such a breakpoint only performs its side effect when your code is running with Delphi's debugger attached to the process. Nonetheless, this type of breakpoint can be extremely useful, especially when you are in the testing phase of your application development.

This article shows you how to create breakpoints that cause side effects. It begins with a general overview of breakpoints, and continues with a discussion of non-breaking breakpoints. Finally, how to create breakpoints to produce side effects is explained in detail.


Breakpoint Overview

When most Delphi developers think of breakpoints, they typically think of the debugger feature that interupts the execution of your code upon executing a specific line of your code. In reality, this definition is far from complete.

First of all, the only type of breakpoint that is associated with a line of your source code is a source breakpoint. There are three other types of breakpoints: address breakpoints, data breakpoints, and module breakpoints.

Address Breakpoints

Address breakpoints permit you to define a breakpoint that will trigger when an instruction at a particular memory address is executed. You can only add an address breakpoint when the debugger is loaded. Then you can select Run -> Add Breakpoint -> Address Breakpoint. Enter the address in the Address field of the Add Address Breakpoint dialog box.

You can also add an address breakpiont from the disassembly pane of the CPU window. With your project stopped in the debugger, display the CPU window by selecting View -> Debug Windows -> CPU. To add an address breakpoint, either click in the left gutter of the disassembly pane in the CPU window on the line associated with the instruction address, press Ctrl-B with your cursor on the instruction line, or right-click the instruction line and select Toggle breakpoint. An address breakpoint is shown in the CPU window in Figure 1.


Figure 1 An address breakpoint in the CPU window


Data Breakpoints

Data breakpoints are those that trigger when a particular memory address is written to. The memory address can be represented either as a hexadecimal number or as the variable used to refer to that memory address. Unlike other breakpoint types, data breakpoints last only for the current Delphi session.

To add a data breakpoint, invoke the debugger (either through an exception or by hitting a breaking breakpoint). Then select Run -> Add Breakpoint -> Data Breakpoint. Delphi responds by displaying the Add Data Breakpoint dialog box, as shown in Figure 2.


Figure 2 Adding a data breakpoint

At Address, enter the memory address of the data. Use Length to define the length of the data beginning at that address. If you are running Delphi 2007 or later, you can also enter the name of a variable in the Address field.

In earlier versions of Delphi, to add a data breakpoint based on a variable name, add the variable into the Watches window. (You display the Watches window by selecting View -> Debug Windows -> Watches from Delphi's main menu.) Then, right-click on the watch entry and select the option Break on change. For data breakpoints added using this technique, you can modify the breakpoint properties by right-clicking on the breakpoint in the Breakpoint List dialog box and selecting Properties. (You can display the Breakpoint List dialog box by pressing Ctrl-Alt-B.)

Module Breakpoints

Module load breakpoints are those that trigger when a specified module is being loaded into your application. For example, if you want to ensure that a particular DLL is not being used by your application, you can set an enabled breaking module breakpoint for it, ensuring that the debugger will load if for some reason that DLL gets loaded by your application.

To add a module breakpoint, select Run -> Add Breakpoint -> Module Load Breakpoint. Delphi responds by displaying the dialog box shown in Figure 3.



Figure 3 The Add Module Breakpiont dialog box

Enter the name of the module in the Module Name field. Click OK when you are done. This is the only way to set a module load breakpoint on a module that is not yet loaded (or even one that will never be loaded).

You can also create a module load breakpoint from the Modules window, but this is only useful if the project is in the debugger and the module has already been loaded. With the debugger loaded, open the Modules window by selecting View -> Debug Windows -> Modules. Then in the Module pane, right-click the module you want to break on, and select Break On Load. Modules that will trigger a module load breakpoint appear in the Module pane with a red dot to their left. To remove a module load breakpoint, right-click the specific module and select Break On Load once again.

Non-Breaking Breakpoints

Another aspect of the breakpoint definition provided at the top of this article that is not completely correct is the part where the breakpoint interupts the execution of your code. In fact, a breakpoint only interupts the execution of your code if it is a breaking breakpoint. Non-breaking breakpoints, like breaking breakpoints, trigger when they are active and are encountered by the debugger. Unlike breaking breakpoints, however, the do not stop code execution.

To define a non-breaking breakpoint, display the Breakpoint Propeties dialog box for the breakpoint. Click the button labeled Advanced >> to display the Advanced breakpoint properties, as shown in Figure 4, and then disable the Break checkbox. (Module breakpoints are the only breakpoints that do not support a non-breaking mode.)


Figure 4 The Advanced view of the Breakpoint Properties dialog box

At first you might wonder "What is the value of a breakpoint if it does not stop your code execution?". Let me tell you, the value can be very big indeed. Non-breaking breakpoints permit you to perform a variety of actions without interrupting the execution of your application.

Consider that a non-breaking breakpoint can be used to enable or disable one or more other breakpoints. For example, you can set a non-breaking breakpoint to execute on the second pass of a section of initialization code that you assume will only execute once. If that code is executed a second time, for whatever reason, you can enable a group of additional breakpoints that will load the debugger, permitting you to examine your application's variables in order to determine why the code is executing a second time.

For me, however, the most valuable capability of a non-breaking breakpoint is to perform a side effect.

Breakpoints with Side Effects

There are five types of side effects that a breakpoint can perform, whether or not it is a breaking breakpoint. It can enable or disable the loading of the debugger in response to an exception, it can write a message to the event log, it can evaluate an expression, it can enable or disable breakpoints associated with a particular group, and in the more recent versions of Delphi, it can write some or all of the call stack to the event log.

Of these, the side effect I consider the most valuable is the ability to evaluate an expression, so I will consider that side effect last. But first, let's consider the other side effects.

Controlling How The Debugger Handles Exceptions

If you check the Ignore subsequent exceptions checkbox, the integrated debugger will stop loading when an exception is raised, at least until another breakpoint that enables subsequent exceptions is encountered. To define a breakpoint that enables exceptions, place a second breakpoint (breaking or non-breaking), and check the Enabled subsequent exceptions checkbox.

Disabling and enabling exceptions using non-breaking breakpoints permits you to conveniently run a section of code from the IDE that raises exceptions, without interruption from the debugger. This technique is particularly useful for sections of code that raise exceptions for the purpose of displaying errors or warnings to the user, but which do not constitute bugs or other similar errors in your code.

For example, when client-side validation code (code that tests the validity of user input) raises an exception, it is really not an error, code-wise. Instead, it is to inform the user that validation has failed, suggest to the user how to correct the problem, and to abort any remaining validations or operations that would proceed had the user's input been valid. While testing your validation code it serves you no purpose to be interrupt by the integrated debugger.

Writing Messages to the Event Log

Use the Log message field to enter a string that will be written to the event log when the non-breaking breakpoint triggers. This option is a nice alternative to using the Windows API call OutputDebugString, which requires adding additional code to your project. After running your application in the IDE, or while the integrated debugger is loaded, you can view messages written to the event log using the Log message field by selecting View -> Debug Windows -> Event Log. Messages written to the event log using breakpoints are identified by the label "Breakpoint Message."

Enabling/Disabling Breakpoint Groups

A breakpoint can be used to enable and disable groups of breakpoints. A breakpoint group consists of one or more breakpoints to which you have assigned the same group name, using the Group field on the Breakpoint Properties dialog box (see Figure 4).

When a breakpoint group is disabled, none of the breakpoints in the group will trigger when encountered by code running in the IDE. Enabling a group restores the enabled property of any disabled breakpoints in the group.

Evaluating Expressions

The Eval expression field serves two purposes. First, it can be used to evaluate any expression. This expression can include any object, method, variable, constant, resource, function, or type that is within scope of the breakpoint, and can also include operators appropriate for those symbols. After having entered a value in Eval expression, you have the option to enabled the Log result checkbox. When Log result is checked, the value of the expression is written to the event log. As with the Log Message field, Eval expression results that are logged permit you to avoid writing expression to the event log using OutputDebugString.

So, finally, here is the main point of this article. My favorite use of Eval expression is to execute a function. Specifically, a function that has side-effects. For example, imagine that upon startup your application tests for the existence of a CDS file that is used by a ClientDataSet. If the file is absent, you call CreateDataSet to create the data structure that the ClientDataSet will use, after which that structure is written to a CDS file.

During testing, you may always want to begin your application without an existing CDS file, so that you can test whether or not the CDS file is being created properly. One way of doing this is the write a function that deletes the CDS file if it exists. You can then call this function with a breakpoint by adding the function name to the Eval expression field.

So long as this Eval expression is associated with a breakpoint that is encountered prior to the first time your application tests for the presence of the CDS file, the Eval expression function will always ensure that no file exists when the test occurs. Since breakpoints only trigger when the application is run from the IDE, a deployed application, or one executed without the debugger being enabled, will not destroy an existing CDS file.

In order to execute a function using the Eval expression field, that function must be included in your compiled code by the linker. But this is a bit more complicated than it sounds. Specifically, if the only call to your function that you create to produce your side effect is made by the breakpoint, Delphi's smart linker will not recognize that the function is used, and will therefore omit it from the compiled application. While this feature ensures that your applications only contain code that they use, thereby keeping your exe size to a minimum, it will have the effect of removing your side effect.

Fortunately, there is a work-around for this. Specifically, you can trick the smart linking into thinking that your side effect function might be called, in which case it will be obligated to include the function in your compiled code. You do this by creating a method that looks like an event hander, and include a call to your side effect function within that method.

Here is how you can do this. Create a published method. While in reality this method can have any signature, you might as well make it a TNotifyEvent, which is a procedure that takes a TObject parameter named Sender. Then, in the implementation of this method, include your call to your side effect function. The following code shows what the pseudo event handler, and the side effect function, might look like:


procedure TForm1.FakeEvent(Sender: TObject);
begin
CleanupCDS;
end;

function TForm1.CleanupCDS: String;
begin
if FileExists(ExtractFilePath(Application.ExeName) + 'data.cds') then
DeleteFile(ExtractFilePath(Application.ExeName) + 'data.cds');
end;

All you have left to do is to add the call to CleanupCDS to the Eval Expression property of the breakpoint, as shown in Figure 5. Assuming that the Break property of the breakpoint has been disabled, anytime that this breakpoint is encountered in your code while executing with the debugger active, the CDS file named DATA.CDS, located in the same directory as your application, will be silently deleted.


Figure 5 A non-breaking breakpoint with side effects

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.