When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article

ASP ASP.NET ASP FAQs Message Board Feedback
 
Print this Page!
Published: Wednesday, November 4, 2009

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.

  • Part 1 - learn about how the membership features make providing user accounts on your website a breeze. This article covers the basics of membership, including why it is needed, along with a look at the SqlMembershipProvider and the security Web controls.
  • Part 2 - master how to create roles and assign users to roles. This article shows how to setup roles, using role-based authorization, and displaying output on a page depending upon the visitor's roles.
  • Part 3 - see how to add the membership-related schemas to an existing database using the ASP.NET SQL Server Registration Tool (aspnet_regsql.exe).
  • Part 4 - improve the login experience by showing more informative messages for users who log on with invalid credentials; also, see how to keep a log of invalid login attempts.
  • Part 5 - learn how to customize the Login control. Adjust its appearance using properties and templates; customize the authentication logic to include a CAPTCHA.
  • Part 6 - capture additional user-specific information using the Profile system. Learn about the built-in SqlProfileProvider.
  • Part 7 - the Membership, Roles, and Profile systems are all build using the provider model, which allows for their implementations to be highly customized. Learn how to create a custom Profile provider that persists user-specific settings to XML files.
  • Part 8 - learn how to use the Microsoft Access-based providers for the Membership, Roles, and Profile systems. With these providers, you can use an Access database instead of SQL Server.
  • Part 9 - when working with Membership, you have the option of using .NET's APIs or working directly with the specified provider. This article examines the pros and cons of both approaches and examines the SqlMembershipProvider in more detail.
  • Part 10 - the Membership system includes features that automatically tally the number of users logged onto the site. This article examines and enhances these features.
  • Part 11 - many websites require new users to verify their email address before their account is activated. Learn how to implement such behavior using the CreateUserWizard control.
  • Part 12 - learn how to apply user- and role-based authorization rules to methods and classes.
  • Part 13 - see how to create a login screen that allows Admin users to log in as another user in the user database.
  • Part 14 - learn how to create a page that permits users to update their security question and answer.
  • Part 15 - the Membership API does not provide a means to change a user's username. But such functionality is possible by going directly to the user store, as this article illustrates.
  • Part 16 - the Membership system includes the necessary components for enforcing expiring passwords. This installment shows how to implement such a policy.
  • Part 17 - see how to display important, unread announcements to users when they sign into the website.
  • Part 18 - often, applications need to track additional user information; learn how to capture this information in a database and see how to build pages to let users update their own information and to display this information to others.
  • (Subscribe to this Article Series! )

    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!

    - continued -

    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.

    ColumnData TypeNotes
    AnnouncementIduniqueidentifierPrimary key; default value of NEWID()
    Subjectnvarchar(50) 
    Bodynvarchar(MAX) 
    DateCreateddatetimeDefault value of getutcdate()
    Activebit 

    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.
    The following screen shot show the ManageAnnouncements.aspx page in action.

    Manage announcements.

    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 - &lt; and &gt;. 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 the LastLoginDate 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.
    As I just noted, after the user is authenticated the 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

    Protected Sub myLogin_LoggingIn(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.LoginCancelEventArgs) Handles myLogin.LoggingIn
       'Get info about the user
       Dim usrInfo As MembershipUser = Membership.GetUser(myLogin.UserName)

       If usrInfo IsNot Nothing Then
          'This user account exists... "save" their last login date so that it can be examined in the LoggedIn event handler
          usersLastLoginDate = usrInfo.LastLoginDate.ToUniversalTime()
       End If
    End Sub

    Protected Sub myLogin_LoggedIn(ByVal sender As Object, ByVal e As System.EventArgs) Handles myLogin.LoggedIn
       ... Some data access code removed for brevity ...

       'Determine the most recent announcement date
       Dim mostRecentAnnouncementDate As DateTime = ... Get Most Recent Announcement Date ...

       'See if this user's last login date precedes the most recent announcement
       If usersLastLoginDate < mostRecentAnnouncementDate Then
          Response.Redirect(String.Format("~/Announcements.aspx?AnnouncementsSince={0}&RedirectUrl={1}", _
                                  Server.UrlEncode(usersLastLoginDate), _
                                  Server.UrlEncode(FormsAuthentication.GetRedirectUrl(myLogin.UserName, False))))
       End If
    End Sub

    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 by Announcements.aspx to determine which announcements need to be displayed, and
    • RedirectUrl - the URL the login page was going to send the user to after logging in. This value is passed to the Announcements.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 the FormsAuthentication.GetRedirectUrl method.
    Also note that the parameters injected into the querystring are appropriately encoded via the 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)
    FROM Announcements
    WHERE DateCreated <= getutcdate()

    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]
    FROM [Announcements]
    WHERE (([Active] = @Active) AND
          ([DateCreated] >= @DateCreated) AND
          ([DateCreated] <= getutcdate()))
    ORDER BY [DateCreated]

    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">
       <%#Server.HtmlEncode(Eval("Subject").ToString())%>
    </div>
    <div class="announcementBody">
       <%#FormatAnnouncementBody(Eval("Body").ToString())%>
    </div>

    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
       Dim output As String = Server.HtmlEncode(body)

       output = output.Replace(Environment.NewLine, "<br />")

       Return output
    End Function

    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).

    The user's unread announcements are displayed.

    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!

  • By Scott Mitchell


    Further Reading


  • Accessing and Updating Data in ASP.NET
  • Data Access Tutorials (VB and C# versions available)
  • Website Security Tutorials (VB and C# versions available)
  • Attachments


  • Download the code used in this article

    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.

  • Part 1 - learn about how the membership features make providing user accounts on your website a breeze. This article covers the basics of membership, including why it is needed, along with a look at the SqlMembershipProvider and the security Web controls.
  • Part 2 - master how to create roles and assign users to roles. This article shows how to setup roles, using role-based authorization, and displaying output on a page depending upon the visitor's roles.
  • Part 3 - see how to add the membership-related schemas to an existing database using the ASP.NET SQL Server Registration Tool (aspnet_regsql.exe).
  • Part 4 - improve the login experience by showing more informative messages for users who log on with invalid credentials; also, see how to keep a log of invalid login attempts.
  • Part 5 - learn how to customize the Login control. Adjust its appearance using properties and templates; customize the authentication logic to include a CAPTCHA.
  • Part 6 - capture additional user-specific information using the Profile system. Learn about the built-in SqlProfileProvider.
  • Part 7 - the Membership, Roles, and Profile systems are all build using the provider model, which allows for their implementations to be highly customized. Learn how to create a custom Profile provider that persists user-specific settings to XML files.
  • Part 8 - learn how to use the Microsoft Access-based providers for the Membership, Roles, and Profile systems. With these providers, you can use an Access database instead of SQL Server.
  • Part 9 - when working with Membership, you have the option of using .NET's APIs or working directly with the specified provider. This article examines the pros and cons of both approaches and examines the SqlMembershipProvider in more detail.
  • Part 10 - the Membership system includes features that automatically tally the number of users logged onto the site. This article examines and enhances these features.
  • Part 11 - many websites require new users to verify their email address before their account is activated. Learn how to implement such behavior using the CreateUserWizard control.
  • Part 12 - learn how to apply user- and role-based authorization rules to methods and classes.
  • Part 13 - see how to create a login screen that allows Admin users to log in as another user in the user database.
  • Part 14 - learn how to create a page that permits users to update their security question and answer.
  • Part 15 - the Membership API does not provide a means to change a user's username. But such functionality is possible by going directly to the user store, as this article illustrates.
  • Part 16 - the Membership system includes the necessary components for enforcing expiring passwords. This installment shows how to implement such a policy.
  • Part 17 - see how to display important, unread announcements to users when they sign into the website.
  • Part 18 - often, applications need to track additional user information; learn how to capture this information in a database and see how to build pages to let users update their own information and to display this information to others.
  • (Subscribe to this Article Series! )



  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article