Blog with WebSnap

By: Nick Hodges

Abstract: This article continues our series on WebSnap and shows you how to add items to a database using WebSnap. By Nick Hodges.

Now, where were we?

It has been a while since I have submitted an installment in the WebSnap Chronicles. When I last checked in with you, I showed how to create automatic paging for your Web pages, both with grids and by formatting data however you want it. Remember, the trick is that paging works only with grids, and if you want to customize the data, you need to put it into a grid with a single column. I illustrated that for you by creating a Weblog.

The Weblog was all well and good, but it was hard to make additions to it. You either wrote yourself a little application or you used a tool to add your entries manually. But that's no way to blog. You need something handy, something that's always at the ready, so you can pontificate as the mood strikes you. You, my fine programming friend, need to be able to make that profound Weblog entry that will likely change perceptions about the truth of the universe wherever you are. You need a Web app.

Fortunately, you are about to find out how you can update your Weblog from your Web site itself. You'll be able to make entries right into the log that will show up immediately, and since you are an expert at access rights, you'll be the only one who even knows that it can be done.

HOUSEKEEPING

First, however, there are some housekeeping tasks ahead of us. The first is you'll want to fix some, ahem, problems with WebSnap. Corbin Dunn, a QA Engineer with Borland and a dedicated newsgroup participant, has put together a page of fixes and comments on WebSnap that you'll want to look at. For this article, you will definitely want to apply the fixes for the crash that happens when you shut down your app and that irritating "cannot perform this operation on a closed dataset error". If you have applied the first update to Delphi 6.0, then you may want to do some of the other fixes as well. By the way, Corbin has indicated that there will be a second update 2 for Delphi 6, so more fixes to WebSnap are on the way. In addition, Corbin is very responsive to questions and problems on the WebSnap newsgroups (he should really get a raise or a medal or some other fine thing), so if you do have questions and problems, feel free to post there.

Next, make sure that you've gone back to the PagedAdapter article and done the "fixes" to our application that are at the beginning of the article. Make sure to install the new nxEndUserSessionAdapter component, in particular. Then, if you want, you can download the code for this article from Code Central, and the IDE won't complain when you try to load the project. In addition, I have put up a Web page that outlines some WebSnap resources on the Web. Feel free to visit and take advantage of some of the good links there. In addition, if you have any additions to the page that you think ought to be there, be sure to let me know.

Third, this article assumes that you have an existing application in the state it was left in at the end of the PagedAdapter article, and the instructions below assume that you are adding to that application. If not, you can download that application from CodeCentral and start from there.

That ought to do it for catching up and squaring things away, so let's get on to the new stuff you want to learn about.

BLOGGING THE WEBSNAP WAY

Okay, to get this thing going, we'll need to make some changes to the database. In the last article, you used an Interbase database called WEBLOG.GDB. It held basic information about each entry. Now that we are going to be adding items to it, we need a way to get unique values for the ID field. The normal way to do this is to use a generator to guarantee a unique value, and then to access it from the database with a stored procedure. In order to do that, you have to add a generator and a stored procedure. (Duh!) Run the following SQL against the database to add the generator:

CREATE GENERATOR "WEBLOG_ID_GEN";

Then run this SQL to add the stored procedure. If you don't want to bother with that, the code in CodeCentral has the database in it all ready to go. (Note that it also includes some cheesy entries that you might want to delete.)

COMMIT WORK;
SET AUTODDL OFF;
SET TERM ^ ;

/* Stored procedures */

CREATE PROCEDURE "CREATEWEBLOGID" 
RETURNS
(
  "AVALUE" INTEGER
)
AS
BEGIN EXIT; END ^

ALTER PROCEDURE "CREATEWEBLOGID" 
RETURNS
(
  "AVALUE" INTEGER
)
AS
BEGIN
 AVALUE = gen_id(WEBLOG_ID_GEN, 1);
END
 ^

SET TERM ; ^
COMMIT WORK;
SET AUTODDL ON;

YOU KNOW WHAT TO DO

Now that the database is all ready to go, you should add another page to the application. You know what to do. (If you don't know what to do, then go back to some of my earlier articles and review the process of adding pages to your application.) Name the page AddWebLog, give it a TAdapterPageProducer, and save the page as wmAddWebLog.pas. Then limit its access by making the factory call in the initialization section look like this:

initialization
  // Change the AccessRights for this page.
  //  See the article at http://community.borland.com/article/0,1410,27777,00.html for more information about this
  if WebRequestHandler <> nil then
    WebRequestHandler.AddWebModuleFactory(TWebPageModuleFactory.Create(TAddWebLog, TWebPageInfo.Create([wpPublished, wpLoginRequired], '.html', '', '', '', 'CanAddWebLog'), crOnDemand, caCache));

Doing this will allow only users with "CanAddWebLog" in their AccessRights property to view the page. While you are at it, add "CanAddWebLog" to one of your users in the WebUserList component on your Home page. If you forget to do that, then you won't even be able to view the new page you created, and that would be just silly, right?

Now add the following components, naming them according to the table below:

Component New Name
TDatasetAdapter dsaWebLog
TSQLConneection SQLConnection
TSQLStoredProc spNewWebLogID
TSQLClientDataset cdsWebLog

Now take the following steps:

  1. Point the SQLConnection to the WEBLOG.GDB file and set Connected to True. Set the user name and password for your system, and set LoginPrompt to False.
  2. Attach the cdsWebLog to the SQLConnection.
  3. Double-click on the cdsWebLog and add all the fields to the dataset.
  4. Select the ID field and change its ProviderFlags to include pfInKey. (Remember, the DatasetAdapter needs to know what the primary key of its dataset is.)
  5. Set the cdsWebLog Active property to True.
  6. Point the spNewWebLogID to the SQLConnection and set its StoredProcName to CREATEWEBLOGID.

That ought to take care of the database end of things. (Is it just me, or is WebSnap very mouse-click intensive? Man, look at all the steps...!) Now it's off to set up the key component here, the dsaWebLog DatasetAdapter. That's the component that will manage all the data on the Web page. But you know that already, right? Anyway, take the following steps on the dsaWebLog component:

  1. Set the Dataset property to the cdsWebLog component.
  2. Right click on it, select the Field Editor, and add all the fields.
  3. Right click again, but this time select the Actions Editor. Then add the Cancel and Apply actions. Those are the only two you'll need.
  4. Then, double-click on the TAdapterPageProducer and add an Adapter Form.
  5. Select the AdapterForm and add an AdapterErrorList, an AdapterFieldGroup, and an AdapterCommandGroup.
  6. Hook the AdapterFieldGroup to the dsaWebLog component, and then add all the fields except the ID field.
  7. Set the AdapterMode property of the AdapterFieldGroup to Insert.
  8. Set the CaptionPostion for all three fields to capAbove. Set the Caption properties to something more palatable than the ugly default ones.
  9. Hook the AdapterCommandGroup to the AdapterFieldGroup, and add all the actions.
  10. That FldEntryText control doesn't look too good, so select it and set its DisplayRows property to 10, the InputType property to iftTextArea, and the DisplayColumns property to 60.
  11. You'll probably want to be able to the date/time of the new entry, but you won't need to edit it. Therefore, select the FldENTRYDATETIME component and set its ViewMode property to vmDisplay. This will display (after we add some code in a minute) the current date and time, but not allow it to be edited.

Now that you are sick of all that mouse clicking, it's finally time to add a little code. Go back to the Field Editor for the dsaWebLog and select the AdaptENTRYDATETIME field. Add this code to the OnGetDisplayText event handler:

procedure TAddWebLog.AdaptENTRYDATETIMEGetDisplayText(Sender: TObject;
  Field: TField; var Value: String);
var
  DateFormat: string;
begin
  // Display the current date time.  It's not the actual value that will end up
  // in the database, but it gives the user some feedback that the entry is there
  DateFormat := Format('%s %s', [ShortDateFormat, ShortTimeFormat]);
  Value := '<B>' + FormatDateTime(DateFormat, Now) + '</B>';
end;

This will format the text for the date/time in the web page. Next, add this to the OnGetValue event of the AdaptTITLE:

procedure TAddWebLog.AdaptTITLEGetValue(Sender: TObject; Field: TField;
  var Value: Variant);
begin
  //  We want these values blank when they show up, as this is a new entry.
  Value := '';
end;

And add this to the OnGetValue of the AdaptENTRYTEXT:

procedure TAddWebLog.AdaptENTRYTEXTGetValue(Sender: TObject; Field: TField;
  var Value: Variant);
begin
  //  We want these values blank when they show up, as this is a new entry.
  Value := '';
end;

Those last two code entries are a little strange. I found that you have to manually enter blank values to the fields that you want to insert, as the default values are the current record in the database. I assure you that the database is in insert mode (remember above when we set the AdapterMode to Insert?), but without the above handlers, the controls will have the values of the last record in them. This is likely a bug and I'll report it to Borland.

Do all that, and you should see something like this in the TAdapterPageProducer:

Now we'll put that stored procedure to work. Add the following public method to the AddWebLog webmodule:

function TAddWebLog.CreateNewWebLogID: integer;
begin
  // Execute the stored procedure and get the result of the generator
  spNewWebLogID.ExecProc;
  Result := spNewWebLogID.Params.ParamValues['AValue'];
end;

While you're at it, add this code to the dsaWebLog's AfterUpdateFields event:

procedure TAddWebLog.dsaWebLogAfterUpdateFields(Sender: TObject;
  ActionRequest: IActionRequest; AdapterFields: TObjectList);
begin
  if cdsWebLog.UpdateStatus = usInserted then
  begin
    // Fix up the text that comes through.
    // Get a unique ID value
    cdsWebLogID.AsInteger := CreateNewWebLogID;
    // Set the entry date/time to now
    cdsWebLogENTRYDATETIME.AsDateTime :=  Now;

    // Make sure there aren't any characters in the text that will gum up a webpage.
    cdsWebLogTITLE.AsString := EscapeText(cdsWebLogTITLE.AsString);
    cdsWebLogENTRYTEXT.AsString := EscapeText(cdsWebLogENTRYTEXT.AsString);
  end;
end;

Actually, this is the key code. This is where the information is actually sent to the database. The entry's date and time are set, and then the text for the title and the entry is cleaned up. The EscapeText function, found in the WebSnapUtils.pas unit, replaces the <, >, & and " characters with their HTML equivalents.

You need to make sure that the ClientDataset always reconciles itself back to the database, so whenever there is an action, either Apply or Cancel, you need to run this code in the Web module's AfterExecuteAction event handler. Do so as follows:

procedure TAddWebLog.dsaWebLogAfterExecuteAction(Sender,
  Action: TObject; Params: TStrings);
begin
  // Clientdatasets need to be applied all the way back to the server, or whatever is on the
  // other end of the DatasourceProvider.  If there is a modification waiting, apply it.  If not,
  // cancel the transaction
  try
    if cdsWebLog.UpdateStatus <> usUnmodified then
    begin
      cdsWebLog.ApplyUpdates(-1);
    end else
    begin
      cdsWebLog.Cancel;
    end;
  except
    on E: Exception do
    begin
      // Add any exceptions to the Adapter's Error property, so they will show up in a
      // TErrorList 
      dsaWebLog.Errors.AddError(E);
    end;
  end;
end;

WILL PROPERTIES NEVER CEASE?

Okay, more property settings. Once the entry is complete and you press the Apply button, you'll likely want to see your handiwork right away. The way to do that -- and the way to make any command go to another page -- is to set the PageName property of the Command. Go to the Web Surface Designer of the TAdapterPageProducer and change the PageName property of both commands to WebLogPage. This will cause the application to go to the WebLog when either button is pushed.

It is easy to get confused between Actions and Commands. Actions are part of a TAdapter, and they define specific actions of the TAdapter. They can be called manually in your server-side script. Commands are the HTML representation of Actions in a TAdapterPageProducer, and are normally represented by buttons or links on an HTML page. I do find that I sometimes bounce between an Action and the Command representing that action looking for a property, often guessing wrong and going to the Action when I should have gone to the Command, and vice-versa. After a while you'll start to get the hang of it. Generally, if it has to do with the appearance on the Web page, it is a Command Property, and if it has to do with handling a value or the code behind an action, it is a property or event of the Action.

In keeping with this, you will probably want a button on your WebLog page that will allow you, and only you, to move to the AddWebLog page. To do that, take the following steps:

  1. Go to the WebLogPage and a regular AdapterAction to the WebLogPagedAdapter found there.
  2. In order to limit access to the button, set its ExecuteAccess property to 'CanAddWebLog' -- the same value you used above to limit access to the AddWEbLog page.
  3. Open the Web Surface Designer in the TAdapterPageProducer and add a whole new AdapterForm, AdapterFieldGroup, and AdapterCommandGroup.
  4. Connect the new AdapterFieldGroup to the WebLogPagedAdapter and set its AddDefaultFields property to False.
  5. Set the AdapterCommandGroup's DisplayComponent property to the new AdapterFieldGroup.
  6. Right-click on the new AdapterCommandGroup and select "Add Commands..." Add the AdapterAction component.
  7. Select the CmdAdapterAction and set its Caption to "Add a WebLog Entry."
  8. Set its PageName property to AddWebLog, the name of the page you use to add WebLog entries.
  9. Most importantly, to keep unauthorized users from seeing the button: In the HideOptions property, set bhoHideOnNoExecuteAccess to True. This will hide the button unless the user has the proper AccessRights value. The cool thing here is that only authorized users will know that there is a way to add items. Unauthorized users will never even see the AddWebLog age or the button to get there.

FINALLY, IT OUGHT TO WORK

Man, that is a lot of work. And the really nutty thing is, if you miss one thing, the whole app probably won't work right. I hope I didn't miss any steps in describing it to you! Anyway, if you compile the application and run it, you should be able to log in as your "special" user and see the AddWebLog page.

Add a new entry title and text, then hit the Apply button. You should see your new entry on your WebLog page. If you are on the page and decide you don't want to make the entry, hit the Cancel button, and all is forgotten.

There you go! From anywhere in the world, you can add an entry to your Weblog...right from your own Web site. Gotta love that.

Nick Hodges is chief technology officer at Lemanix Corporation, a consulting shop specializing in Delphi development. He is a TeamB member and he wants to put a wood-burning stove in his house. But mostly he likes to hang with his family and enjoy their new home in St. Paul, MN.

Server Response from: ETNASC02