Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

BLOGGING WITH DELPHI #3

Continuing our exploration of Delphi 2005, ASP.NET and ECO II to implement a web log application.
by Bob Swart

Requirements:
Borland's Delphi 2005 Architect

 

Download The Source Code:
delphiblog3.zip

 

Continued from Part Two and continues in Part Four

Borland Delphi 2005 Architect contains a featureset called Enterprise Core Objects 2, which allows us developers to create applications based on a model (with objects, inheritance and associations), which can be made persistent in a DBMS, and used to create GUI as well as web applications. In this multi-part article, I’ll use Delphi 2005 and Enterprise Core Objects to define and implement an application handling web logs, also called blogs, this time, with the focus on ASP.NET features and next time I’ll finally cover deployment (on a clean-machine).

Refreshing Memory...

In the first part of this series, I introduced Enterprise Core Objects in Delphi 2005 Architect, and showed how to build the model to create and maintain weblog entries (and feedback). I also explained and demonstrated how to make the instance of the model - called the EcoSpace - persistent in a database like SQL Server, Interbase or any other ADO.NET or BDP compliant DBMS.

In the second part of this series, we built ASP.NET pages on top of the objects in the EcoSpace, showing the list of categories, weblog posts in categories, and allowing visitors to read the posts (leaving comments wasn’t possible, but will be covered shortly).

This time, we’ll add some management capabilities to the application, including authentication and authorization (who can add and/or edit what), and I’ll add the comment-leaving feature as well.

Continuing with the Weblog project

If you start with the source code from last time, and reopen the Weblog project, you may notice that it opens up the project file (for library Weblog), and not the ASP.NET pages. Last time, we created two .aspx pages: the first page called Blogs.aspx, and the second one called Blog.aspx. The Blogs page shows a list of categories and, once we select a category, a list of blog posts. When selecting a blog post, we get redirected to the Blog.aspx page, showing the actual blog contents. The latter is also the place where I want visitors to leave comments. However, the current Blog.aspx page includes a Save Changes button, which is meant for the author of the blog entry itself, and not for a visitor leaving comments. So we need some way to identify whether or not the application is dealing with the author of a blog item, or a visitor. This will be done with (optional) author authentication.

Optional Author Authentication

The reason why I call it “optional”, is that I don’t want to place a burden on the visitors of my weblog. Only the author should do something special in order to tell the system that he or she is the original blog author (and not the visitor). So the default state should be for the visitor, and not for the author as we designed Blog.aspx last time.

ASP.NET Authentication can be done in a number of ways. If you take a look inside the web.config file, you’ll notice a section called authentication with the following default contents:

<authentication mode="Windows" />

When the authentication mode is set to Windows, it means that you need to be logged in as a Windows user in order to be able to determine your identity. That’s not very useful, in my view. The alternatives are “None” (also not very useful), “Passport” (nice try, but I don’t want to pay Microsoft to allow authors to authenticate themselves), or “Forms”. The last type of authentication effectively means that we need to build it ourselves. Which is fine by me, since it means we can build it with Delphi itself.

ASP.NET Forms Authentication

ASP.NET Forms Authentication can be chosen by assigning “Forms” to the mode attribute in the authentication node. We can set forms authentication specific properties using an embedded forms node. Usually, people specify a loginUrl attribute here, which points to the page where visitors can login. However, that’s most effective in a situation where you want to force visitors to login (when they visit a page for which authentication is required), and I don’t want to force anything. I only want to invite authors to login, at which time the author-specific functionality becomes available. When not logged in – the default situation – only the visitor functionality is available. So I don’t want to force a login page, and hence don’t need (or want) to specify the loginUrl attribute.

What I can include within the forms node is a credentials subnode, where I can specify the name of the authors and their password. Using a clear text way of storing the passwords, this can be specified as follows:

<authentication mode="Forms">

<forms> <credentials passwordFormat="Clear"> <user name="Bob" password="Swart" /> </credentials>
</forms> </authentication>

So there’s one user (author) called “Bob” with a password equal to “Swart”.

Other passwordFormat values

Although the web.config file is a secure file, that is not accessible from outside the web server (unless it’s hacked in some way), I always feel a bit more secure if I can put the passwords in an encrypted format. Of course, we can also use a password database, or store them somewhere else entirely, but for a weblog that’s overkill (in my humble opinion). For an e-commerce application, I’ll gladly use a database with encrypted passwords. Right now, the web.config file is enough, but I wish to store the password in an encrypted way.

Apart from passwordFormat value Clear, ASP.NET offers two encryption formats, namely SHA1 and MD5. We can encrypt a plain text password to the encrypted version using the HashPasswordForStoringInConfigFile method from the FormsAuthentication object (available in the System.Web.Security namespace). I’ve created a little console application in Delphi to help me produce the encrypted password based on the plain version, as follows:

program HashPassword;
{%DelphiDotNetAssemblyCompiler 
 '$(SystemRoot)\microsoft.net\framework\v1.1.4322\system.dll'}
{%DelphiDotNetAssemblyCompiler 
 '$(SystemRoot)\microsoft.net\framework\v1.1.4322\system.web.dll'}
{$APPTYPE CONSOLE}
uses
  System.Web.Security;
var
  Passwd: String;
  F: Text;
begin
  Assign(f,'hash.txt');
  Rewrite(f);
  write('Password: ');
  readln(Passwd);
  writeln(f,'['+Passwd+']');
  write(f,'MD5: ');
  writeln(f,FormsAuthentication.
    HashPasswordForStoringInConfigFile(Passwd, 'MD5'));
  write(f,'SHA1: ');
  writeln(f,FormsAuthentication.
    HashPasswordForStoringInConfigFile(Passwd, 'SHA1'));
  Close(f)
end.

You can compile this little console application on the command-line using dccil, with the following command:

dccil –LUSystem.Web HashPassword.dpr

The resulting executable helps me to produce encrypted passwords if I ever want to change my password. Of course, if my blog system needs to support multiple authors, then I may want to include this functionality in an ASP.NET page, but that’s an exercise for another day.

The bottom line is that I can now modify the authentication node as follows, using an encrypted password for myself (which might be a bit harder to guess):

<authentication mode="Forms">
  <forms>
    <credentials passwordFormat="SHA1">
      <user name="Bob" 
       password="E96E133EDF2417F71AF2CBE9A8E32A29E82791B8" />
    </credentials>
   </forms>
</authentication>

This still means a user called “Bob”, but the password is no longer “Swart” (and no longer very easy to guess either).

(Hidden) Login Page

Although I didn’t specify the loginUrl attribute in the forms node, I still need to add a login page (or at least a login functionality) to the weblog application. In order to hide it from the normal visitors, I just don’t link to it from the application itself, but use it as entry page for authors only.

The page itself is simple, with two TextBox controls for username and password (the second one with the TextMode property set to Password) and a Button, among others, all embedded in a HTML table with three rows of two columns and a width set to 100 percent.


Figure A. Login.aspx

The Button Click event handler is implemented as follows:

procedure TWebForm1.btnLogin_Click(sender: System.Object; 
                                   e: System.EventArgs);
begin
  if FormsAuthentication.Authenticate(tbUsername.Text, 
                                      tbPassword.text) then
  begin
    FormsAuthentication.SetAuthCookie(tbUsername.Text, False);
    Response.Redirect('Blogs.aspx')
  end
end;

After a successful login, the authentication cookie is set (note: this requires the visitor to enable cookies in his or her browser, otherwise the authentication won’t hold), and then we are redirected to the normal start page, namely Blogs.aspx. Authors can now enter using the login page, while regular visitors should use the normal starting page Blogs.aspx right away.

Two-Face Blogs Page

We should now modify the Blogs.aspx page from last time, since by default it shows buttons to create a new Category or a new Post. These buttons should only be visible if the visitor is authenticated (i.e. if the user is an author). This can be controlled in the Page_Load event handler, with the following code, checking to see if the User is authenticated:

procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs);
var
  Id: string;
begin
  EcoSpace.Active := True;
  Id := Request.Params['RootId'];
  if Assigned(Id) and (Id <> '') then
  begin
    rhRoot.SetElement(ObjectForId(Id));
    lbCategory.Text := (rhRoot.Element.AsObject as Category).Name; // BS
  end;
  if not IsPostBack then
    DataBind;
  // TODO: Put user code to initialize the page here
  btnNewCategory.Visible := User.Identity.IsAuthenticated;
  btnNewPost.Visible := User.Identity.IsAuthenticated;
end;

Note that we had to add our custom code at the bottom of the Page_Load event handler, since there’s quite some ECO code in there as well (generated automatically, since the Blogs.aspx page is an ECO ASP.NET page, and not a normal ASP.NET page). A regular visitor (not logged in) will now only see the list of Categories and – for each Category – the list of Posts. The two buttons will only appear if the visitor is authenticated (logged in as author).

Apart from the two “New” buttons, we should also disable (or remove) the Delete command in the DataGrid for the Posts, to avoid that any visitor can delete blogs Posts. This can be implemented in the Page_Load event handler as well, as follows:

dgPosts.Columns[3].Visible := User.Identity.IsAuthenticated

A final feature that I want to add to the Blogs page is the ability to view all Posts when no Category has been selected. This can be done by modifying some of the generated code in the Page_Load event handler. Originally, the code is as follows:

if Assigned(Id) and (Id <> '') then
begin
  rhRoot.SetElement(ObjectForId(Id));
  lbCategory.Text := (rhRoot.Element.AsObject as Category).Name; // BS
end

And we should add an “else” part to this, responding to the situation where the Category is not set, so the rhRoot is not referencing an object to which the ehPosts can run its expression (which is set to self.Posts->orderdescending(Posted) as you may remember). The self here is pointing to the selected Category, but without a selected Category, we should change the OCL expression at run-time to something like Post.allInstances->orderdescending(Posted).

In short, the final version of the Page_Load event handler for the Blogs.aspx page is as follows (with some additional code to report if there are no Posts, yet):

procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs);
var
  Id: string;
begin
  EcoSpace.Active := True;
  Id := Request.Params['RootId'];
  if Assigned(Id) and (Id <> '') then
  begin
    rhRoot.SetElement(ObjectForId(Id));
    lbCategory.Text := (rhRoot.Element.AsObject as Category).Name; // BS
    if (rhRoot.Element.AsObject as Category).Posts.Count = 0 then
    begin
      lbCategory.Text := 'No Posts in category ' + lbCategory.Text;
      dgPosts.Visible := False
    end
    else dgPosts.Visible := True
  end
  else // BS - no category selected
  begin
    lbCategory.Text := 'All Posts';
    ehPosts.Expression := 'Post.allInstances->orderdescending(Posted)'
  end;
  if not IsPostBack then
    DataBind;
  // TODO: Put user code to initialize the page here
  btnNewCategory.Visible := User.Identity.IsAuthenticated;
  btnNewPost.Visible := User.Identity.IsAuthenticated;
  dgPosts.Columns[3].Visible := User.Identity.IsAuthenticated
end;

This will show all Posts, sorted by Posted, when we first enter the Blogs page, just as I wanted.

Adding Comments on Blog Page

Which leads us to the individual Blog page, where the author can enter or modify the text for a Blog, and watch comments written by others (most likely with the ability to delete comments as well). The button “Save Changes” should be made optional, only to be shown when the visitor is authenticated, which can be done by adding the following line of code to the bottom of the Page_Load event of the Blogs.aspx page:

btnSaveChanges.Visible := User.Identity.IsAuthenticated;

In contrast, the normal visitor (but also the author!) should be able to add new comments here. This is typically offered on the same page where the Blog and all previous comments are shown. At the bottom of the Blog page, I’ll place some controls to enter a new comment. Since the comment class was designed (in the first part of this series) to be derived from an Entry, it contains an Author, Title and Posted field. The Author field should be specified by the visitor wanting to place the comment, while the Posted field can be generated by the ASP.NET application itself. The Title field will not be used at this time, but might be used at a later time to verify that a real user is behind the comment (for example by asking to enter a valid e-mail address, or enter the outcome of a simple calculation, and storing that information in the EcoSpace. A bit of an abuse of the Title field, but in retrospect I feel that a comment on a blog post should not have a different title anyway). Apart from the three fields inherited from the Entry class, a Comment class also has the actual “Comments” field, or type string. This is obviously the place where the visitor can leave his or her comments.

So, to cut a long story short, at the bottom of the Blog.aspx page, I’d like to place a TextBox to hold the visitor’s name, and a TextBox to hold the visitor’s comments, plus a Button to add the new comment. Between the top (with the blog text) and the bottom (with the new comments), I’d like to display all existing comments for this blog entry, for which I can use a DataGrid.

We’ll configure the DataGrid and other comments-related controls shortly, but for now the new Blog.aspx page looks as follows at design-time:


Figure B. Blog.aspx

At the top, we see the contents of the actual Blog, followed by a DataGrid that displays the already available comments (if any), and finally at the bottom the place where the visitor can leave new comments.

Add Comment

Clicking on the Add Comment button should create a new instance of a Comment class, fill its properties, connect it to the current weblog post, and finally update the database. This is done as follows:

procedure TWebForm1.btnAddComment_Click(sender: System.Object;
  e: System.EventArgs);
var
  C: Comment;
  P: Post;
begin
  P := rhRoot.Element.AsObject as Post;
  C := Comment.Create(EcoSpace);
  C.Author := tbCommentAuthor.Text;
  C.Comments := tbCommentComments.Text;
  C.Title := P.Title;
  C.Posted := DateTime.Now;
  C.Post := P; // add Comment to Post
  UpdateDatabase;
  DataBind;
end;

Note that we could add some checks here to verify that the Name is specified. This can be done using a RequiredFieldValidator, which is left as an exercise for the reader.

Welcome Author Name

Both the author and all other visitors should be able to leave comments. As a feature for the author, we can automatically fill in the author name (if the author is authenticated), which can be done in the Page_Load as follows:

if not IsPostBack then
   tbCommentAuthor.Text := User.Identity.Name;

Note that we should do this only the first time we enter the page (hence the test for IsPostBack), otherwise we’ll assign the tbCommentAuthor TextBox every time we get back to the page. Also note that if the user is not authenticated, then User.Identity.Name will be empty as well, so this technique will work just fine in all cases.

Display Comments

One thing left to do: the display of existing comments for this particular weblog post. For that, I’ve already placed a DataGrid, but now we must produce the ECO datasource to connect to.

In the non-visual components area of the Blog.aspx page, we have the rhRoot, with property EcoSpaceType set to WeblogEcoSpace.TWeblogEcoSpace and property StaticValueTypeName set to Post. So the root handle of our Blog.aspx page is a Post. In order to find all instances of the Comment class that are connected to the current Post, we need to place an ExpressionHandle component. Call it ehComments, connect its RootHandle property to rhRoot (the current Post), and as Expression return the self.Comments:


Figure C. OCL Expression Editor

Now go to the DataGrid control, and point its DataSource property to ehComments, so the DataGrid will show the Comment fields: Author, Title, Posted, Comments and Post. Actually, I don’t want to see all these fields, so use the Property Builder to delete the Title and Post fields, leaving only Author, Posted and Comments.

If you’ve never used the Property Builder before, just select the DataGrid, and then at the bottom of the Object Inspector click on the “Property Builder” verb. In the DataGrid Properties dialog, go to the Columns page and uncheck the “Create columns automatically at run time” first, after which you can add the Author, Posted and Comments fields.


Figure D. dgComments Properties

This will result in a DataGrid, showing three columns. It may not look very nice at this time, but we can add some user interface customisations later. One thing I’ve added for myself (as author) is a fourth column with the “Delete” button inside, so I can delete comments that are not appropriate. I won’t need the Edit, Cancel and Update buttons at this time, but these can be added later as well when needed (again, only for the author, not for regular visitors).

Apart from the DataGrid Property Builder, we should also use the Auto Format dialog on the DataGrid, in order to give the DataGrid a nice look, like Professional 3 (that we’ve also used in the Blogs.aspx page).

There’s one last thing: the number of comments, which I want to display in the lbComments Label. We need to do that in the Page_Load event handler, but we may also have to update the number of comments (as well as the DataGrid) just after we’ve added a comment, so I’ve added a new method called “UpdateComments” to the web form, and implemented it as follows:

procedure TWebForm1.UpdateComments;
begin
  if (rhRoot.Element.AsObject as Post).Comments.Count = 0 then
  begin
    lbComments.Text := 'No Comments, yet.';
    dgComments.Visible := False
  end
  else
  begin
    dgComments.Visible := True;
    if (rhRoot.Element.AsObject as Post).Comments.Count = 1 then
      lbComments.Text :=
       (rhRoot.Element.AsObject as Post).Comments.Count.ToString + ' Comment'
    else
      lbComments.Text :=
       (rhRoot.Element.AsObject as Post).Comments.Count.ToString + ' Comments'
  end
end;

This method should be called at the end of the Page_Load and the btnAddComment_Click event handlers.

And with that code snippet, let’s end the development phase for now so we can move to the next step: deployment!

Next Time...

Next month, I’ll also finally cover deployment details and issues, resulting in the weblog application that’s live today at http://www.drbob42.com/blog (although it’s not guaranteed to be 24x7 available at this time, since it’s still hosted on my own internet machine as a prototype).

In the deployment article, I’ll cover installation and configuration of SQL Server / MSDE, copying of the database files, creating the virtual directory for the weblog application, and using the deployment manager to deploy the necessary files from the development machine to the web server. And then a process of trial-and-error in order to determine the missing files, until it finally works.

All this and more next times, so stay tuned...

Copyright © 2005 Bob Swart


Bob Swart (aka Dr.Bob - www.drbob42.com) is an author, trainer, developer, consultant and webmaster for Bob Swart Training & Consultancy (eBob42) in The Netherlands, who has spoken at Delphi and Borland Developer Conferences since 1993. Bob has written chapters for seven books, as well as the Borland Delphi 8 for .NET Essentials and Delphi 8. ASP.NET Essentials courseware manuals licensed by Borland worldwide, and is selling his updated Delphi 2005 courseware manuals online at http://www.drbob42.com/training. Bob received the Spirit of Delphi award at BorCon in 1999, together with Marco Cantù.

August 2005

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission