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;
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;
end;
if not IsPostBack then
DataBind;
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;
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;
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
begin
lbCategory.Text := 'All Posts';
ehPosts.Expression := 'Post.allInstances->orderdescending(Posted)'
end;
if not IsPostBack then
DataBind;
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;
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 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
|