Examining ASP.NET's Membership, Roles, and Profile - Part 17
By Scott Mitchell
A Multipart Series on ASP.NET's Membership, Roles, and Profile |
---|
This article is one in a series of articles on ASP.NET's membership, roles, and profile functionality.
SqlMembershipProvider and the security Web controls.aspnet_regsql.exe ).SqlProfileProvider . |
Introduction
Many of the web applications I help build can be classified as in-production line of business applications that receive frequent and ongoing feature enhancements. Typically, these applications have dozens if not hundreds of users who rely on the site each and every day to accomplish tasks necessary to keep the company running smoothly. Every week or so the latest code is deployed to the production servers, bringing with it bug fixes and, very often, new features or changes to existing features. One challenge I've bumped into when working on such applications is how to best alert users of the new features and the changes to existing features?
One useful way to announce any important, system-wide changes is to do so immediately after the user signs into the site. A very simple way to accomplish this would be to automatically redirect all users to an announcements page after signing in, which would list information about the most recent updates to the application. However, this approach desensitizes the user to the announcement page since they are sent there each time they sign in, regardless of whether they've already seen the announcement. A better approach is to determine if there were any announcements made since the user last signed in. If so, redirect the user to a web page that displays just those unread announcements.
This article shows how to implement such a system using ASP.NET's Membership system, the Login control, and a dash of code. A complete working demo is available for download at the end of this article. Read on to learn more!
Creating a Database Table for Announcements
In order to show a user only those announcements that have been made since they last signed in, we need some way to store announcements and to associate a date with them. Perhaps the most flexible approach is to store the announcements in a database. If you download the demo available at the end of this article you'll find that the application contains a database named
ASPNETDB.mdf
in its App_Code
folder. This database includes both the database objects used by the SqlMembershipProvider
along with the Announcements
table I created.
The Announcements
table's schema (shown below) is fairly simple. Each announcement is uniquely identified by a uniqueidentifier column (AnnouncementId
), and
each announcement has a subject and body, modeled by the Subject
and Body
columns. The DateCreated
field indicates the
UTC date and time that the announcement was created, while the Active
bit field provides a
simple mechanism for announcements to be "turned off" without deleting them from the database.
Column | Data Type | Notes |
---|---|---|
AnnouncementId | uniqueidentifier | Primary key; default value of NEWID() |
Subject | nvarchar(50) | |
Body | nvarchar(MAX) | |
DateCreated | datetime | Default value of getutcdate() |
Active | bit |
Anytime a new announcement is needed - whether there is a new rollout with new features to note or, perhaps, some business policy change that users must be made aware of -
a new record should be added to the Announcements
table. As we'll see later on in this article, when a user signs in he is shown all announcements that have
been made since he last signed in. As a result, you don't need to worry about removing old announcements from the Announcements
table. You can leave them in
because users who have already seen them will never see them again.
Creating and Managing Announcements
The demo includes a simple ASP.NET page for creating and managing the records in the
Announcements
table (~/UsersOnly/ManageAnnouncements.aspx
).
This page contains a DetailsView, GridView, and SqlDataSource control that allow the user to:
- Add new announcements,
- Toggle the
Active
bit field of existing announcements, and - Delete announcements.
ManageAnnouncements.aspx
page in action.

Because the focus of this article is on displaying unread announcements to users when they sign on, I don't plan on exploring this page in detail. For a thorough look at using the GridView and DetailsView controls to display, insert, update, and delete data, refer to my article series Accessing and Updating Data in ASP.NET, along with my Data Access Tutorials.
Before we move on there are a couple of things I want to point out. First off, I have it so that the announcement's subject and body are plain text. When we examine the page
that displays the unread announcements to the user who just signed on we'll see that the subject and body's content are HTML encoded, meaning that any HTML characters -
such as <
and >
- are converted into their escaped equivalents - <
and >
. What that means is that
if you type in HTML characters into the subject or body they will be literally displayed in the output.
Additionally, note that the ManageAnnouncements.aspx
page is located in the ~/UsersOnly
folder. This folder uses URL authorization rules in
the Web.config
file in the ~/UsersOnly
folder to allow access to any authenticated user. Consequently, any user who can sign into the site can visit
the ManageAnnouncements.aspx
page and add and delete announcements. Chances are, you'd want to put this page in a folder that is locked down to users in
an administrative role. (Refer to Part 2 of this article series for an in-depth look at roles and
role-based authorization, or check out my Website Security Tutorials.)
Determining Whether A User Has Unread Announcements
Whenever a user signs into the website we need to determine whether there are any unread announcements. To accomplish this we need to determine the date and time the user last signed in and compare that to the date and time of the most recent announcement. If the user's last sign in time predates the most recent announcement then there exists at least one unread announcement. However, if the user's last sign in time is later than the most recent announcement then there are no unread announcements.
The Membership system automatically tracks users' last login date/times, specifically via the MembershipUser
class's LastLoginDate
property. Keep in mind
that whenever a user is authenticated the Membership system automatically updates this value to the current UTC date and time. Therefore, it's imperative that we capture this
value before the user is actually authenticated so that we get the date and time they logged in prior to their current log in.
If you are using the Login Web control to authenticate users then you will need to create two event handlers:
- One for the
LoggingIn
event, where we will get and store theLastLoginDate
value for the user signing into the site, and - One for the
LoggedIn
event, where we will determine the date of the most recent announcement and compare that to the user's last login date, redirecting the user to a page to view the announcements, if necessary.
LastLoginDate
value reflects the current UTC date and time. That is the reason why we will need to create an
event handler for the Login control's LoggingIn
. The LoggingIn
event fires before the user is authenticated and gives us an opportunity to get the
date and time of their last login. If we waited until the LoggedIn
event, which fires after the user is authenticated, the LastLoginDate
value would
be the current UTC date and time and not the date and time the user had previously signed in.
The following code shows the LoggingIn
and LoggedIn
event handlers. I've removed the data access code from the LoggedIn
event handler
for brevity.
Dim usersLastLoginDate As DateTime = DateTime.MaxValue
|
Note that the usersLastLoginDate
variable is defined outside of the two event handlers, meaning that both event handlers have access to this variable.
The LoggingIn
event handler fires first, after the user enters their username and password and clicks the "Log In" button. This event handler starts by
using the Membership.GetUser(username)
method to retrieve information about the user signing into the site. Of course, at this point we don't know if
the user credentials supplied are valid. The person signing into the site may have entered a username that does not exist in the database. For that reason, it's vital that
we ensure that the Membership.GetUser(username)
method actually returned a MembershipUser
object before we attempt to read the
LastLoginDate
value. Assuming there is a user account with the specified name, we store the UTC value of the LastLoginDate
property to the
usersLastLoginDate
variable.
The LoggedIn
event fires after the LoggingIn
event and only fires if the supplied credentials are valid. In this event handler we start by
getting the UTC date and time of the most recent announcement. (I've removed this data access code from the above code snippet for brevity, but we'll come back to it shortly.)
This value is then compared against the user's last login date value retrieved from the LoggingIn
event handler. If the user's last login date precedes the
most recent announcement date then we redirect the user to Announcements.aspx
, which will show the reader her unread announcements.
Note that in the redirect to Announcements.aspx
we are passing along two querystring values:
AnnouncementsSince
- the UTC date/time value of when the user last logged into the site, which is used byAnnouncements.aspx
to determine which announcements need to be displayed, andRedirectUrl
- the URL the login page was going to send the user to after logging in. This value is passed to theAnnouncements.aspx
page so that after the user reads the announcements she can continue onto the page she would have been sent to had there been no unread announcements. The value for this parameter comes from theFormsAuthentication.GetRedirectUrl
method.
Server.UrlEcode
method. Whenever you dump a variable into the
querystring you should always be sure to UrlEncode
it for reasons discussed in this article: Using
Server.UrlEncode
.
Determining the Date and Time of the Most Recent Announcement
In addition to adding the
Announcements
table to the ASPNETDB.MDF
database, I also created a stored procedure named MostRecentAnnouncementDate
that returns the date and time of the most recent announcement (more specifically it returns the date and time of the most recent announcement whose DateCreated
value is less than or equal to the current UTC date and time). The query used by the MostRecentAnnouncementDate
stored procedure follows:
SELECT MAX(DateCreated)
|
The LoggedIn
event handler code in the demo uses ADO.NET to connect to the database and execute the MostRecentAnnouncementDate
.
Displaying Unread Announcements
The final piece of the puzzle is the
Announcements.aspx
page, which displays the unread announcements. Recall that the user reaches this page from the
login page (~/Login.aspx
) if there are more recent announcements since they last logged on. Moreover, two bits of information are passed to the Announcements.aspx
via the querystring: the UTC date and time the user last logged on (AnnouncementsSince
) and the URL the user would have been sent to had there been no unread
announcements (RedirectUrl
).
The Announcements.aspx
page contains a Repeat Web control that's bound to a SqlDataSource control. The SqlDataSource control executes the following ad-hoc query:
SELECT [Subject], [Body]
|
The @Active
parameter's value is set to True
in the SqlDataSource control's SelectParameters
, thereby only retrieving active announcements.
The @DateCreated
parameter's value is assigned to the AnnouncementsSince
querystring value. That conditional, in conjunction with
[DateCreated] <= getutcdate()
, returns those announcements that have occurred after the user last logged on but before the current date and time.
In total, this query returns all active announcements between the user's last login date and the current date/time, ordered so that the oldest announcements are displayed first.
The Repeater control displays these results using a series of <div>
elements. Here's the markup specified in the Repeater's ItemTemplate
:
<div class="announcementSubject">
|
Note that the Subject
field's output is HTML encoded via the Server.HtmlEncode
method. This ensures any markup entered into the subject line
for the announcement is appropriately escaped. The Body
field is not displayed directly, but rather passed into the FormatAnnouncementBody
formatting function, which is defined in the ASP.NET page's code-behind class:
Protected Function FormatAnnouncementBody(ByVal body As String) As String
|
As you can see, this method takes the body value, HTML encodes it, and replaces carriage returns with a <br />
tag.
An End-To-End Example...
With all of these pieces in place, let's look at the user experience. Imagine that we have created two announcements, one on October 25th, 2009 at 1:00 PM GMT and the other on October 27th, 2009 at 4:02 PM GMT. Assume that a user signs into our site on October 28th, 2009 3:15 PM GMT, and that the time he signed on before that was on October 21st 2009 9:45 AM GMT.
Because the user's last login date and time (October 21st 2009 9:45 AM GMT) precedes the most recent announcement date and time (October 27th, 2009 at 4:02 PM GMT) the
user is automatically redirected from the login page to the Announcements.aspx
page, where he is shown both announcements (because both announcements were
created after his previous login date/time).

If the user had last logged in on October 26th 2009 11:45 AM GMT, he would still be redirected to the Announcements.aspx
page because his last login date/time
precedes the most recent announce date and time (October 27th, 2009 at 4:02 PM GMT), but the Announcements.aspx
page would show only one announcement. The
older announcement - the one from October 25th, 2009 at 1:00 PM GMT - would not be displayed because it was created prior to the user's last login date time. Presumably,
the user saw this announcement when he last logged in on the 25th.
As the screen shot above shows, the Announcements.aspx
page also contains a Continue button. This Button's Click
event handler does a
Response.Redirect
to the URL specified via the RedirectUrl
querystring parameter, which sends the user off to the page they would have originally
been directed to had there been no new announcements for them to read.
Ideas for Enhancements
The code and concepts put forth in this article provide a robust, easy to use announcement system. In my projects I've implemented a number of bells and whistles that are not shown here, including functionality like:
- Rich announcement messages - the use of a rich textbox and the ability to include HTML in the announcement body. This means that announcements can have formatted text, including images and hyperlinks.
- Role-based announcements - currently, all announcements are shown to all users. However, certain new features or important messages might be targeted to specific classes of users. With a bit of work you could augment this system so that when creating an announcement you can (optionally) specify that the announcement applies only to a certain subset of roles. Then, only users in those roles would see those announcements.
- Adding "I have read this announcement" CheckBoxes - one site I worked on used announcements to display important legal messages to its end users. It was important
that they had proof that their users had read these announcements and didn't somehow skip over them or simply click "Continue" without reading them. To this end we
enhanced the Repeater in the
Announcements.aspx
page to include a CheckBox titled "I have read this announcement" along with each announcement. The user could not continue until each announcement's checkbox was checked. Moreover, we'd record in the database the date and time the user checked this checkbox.
Happy Programming!
Further Reading
Attachments
A Multipart Series on ASP.NET's Membership, Roles, and Profile |
---|
This article is one in a series of articles on ASP.NET's membership, roles, and profile functionality.
SqlMembershipProvider and the security Web controls.aspnet_regsql.exe ).SqlProfileProvider . |