Filling in the Gaps - Problem Design Solution 3.5 - Part 3
Prologue
In a couple previous posts (here and here), we looked in depth at the code in my ASP.Net Website Programming: Problem – Design – Solution book attempting to fill in any of the gaps that I found (such as where does the code in the book go – what file – and adding code that is missing from the book). This post continues this series by addressing Chapter 4 regarding membership. While I found this chapter much less discombobulated than chapter 3, there are still a few things that need additional attention.
Problem
In this post I will attempt to address these missing pieces (or at least what I deem to be the missing pieces):
- Location for the OpenID class.
- Some missing methods of the OpenID class.
- Adding data to the userProfile control’s country and state DropDownLists.
- The missing GetProfile() method of the userProfile control.
- The missing GetAvatarUrl() method.
- Login Box – where does it go?
- Registration Page - “Name Email not declared”.
- Password Recovery Page
- Location for the AJAX Login Dialog
- Code corrections for AJAX Login Dialog
- Code corrections for persisting the Theme
- Location for Profile_MigrateAnonymous
- Location for the AdminMenuItems Class
- The AdminMenuItem Class (singular)
- Location for BindNavItems
- Guts of the ManageUsers page
- AddEditUsers Page
- ManageRoles Page
- AddEditRole Page
Solution
With no further ado, let's get started...
Where Do I Put the OpenID Class
On page 189, in the last code block, there is a reference to OpenID.IsOpenIdRequest(). This references the OpenID class that hasn’t been defined yet (ok, pet peeve, if a custom class is used in a code sample, it’s nice to define the custom class first so I can understand what’s going on in the custom class and that will help me understand better what’s going on in the code… plus then I can do Intellisense and not mess up spellings. – Sorry for the tangent). This OpenID class begins to be defined in on page 191 as part of the ‘Implementing OpenId Authentication’ section started on page 190.
So the question still stands, where do we put this code? In the BLL code project, within a ‘Security’ folder. The file is named OpenId.vb.
The OpenId Missing Methods
Continuing in the chapter, we come to the OpenId’s Login function.
This method calls the GetIdentityServer function that is, unfortunately, undefined in the book. This function can be found in the code download and is defined:
Private Shared ReadOnly REGEX_LINK As New Regex _
("<link[^>]*/?>", RegexOptions.IgnoreCase Or RegexOptions.Compiled)
Private Shared ReadOnly REGEX_HREF As New Regex _
("href\s*=\s*(?:""(?<1>[^""]*)""|(?<1>\S+))", _
RegexOptions.IgnoreCase Or RegexOptions.Compiled)
Public Shared Function GetIdentityServer(ByVal identity As String) _
As StringDictionary
Using client As New WebClient()
Dim html As String = client.DownloadString(identity)
Dim col As New StringDictionary()
For Each regMatch As Match In REGEX_LINK.Matches(html)
AssignValue(regMatch, col, "openid.server")
AssignValue(regMatch, col, "openid.delegate")
Next
Return col
End Using
End Function
You’ll also notice that we need to define a Regex variable outside our function as well, (actually there are two, one we won’t use till later). The GetIndentityServer function also calls another subroutine that is not defined in the book, AssignValue:
Private Shared Sub AssignValue(ByVal linkMatch As Match, _
ByVal col As StringDictionary, ByVal name As String)
If linkMatch.Value.IndexOf(name) > 0 Then
Dim hrefMatch As Match = REGEX_HREF.Match(linkMatch.Value)
If hrefMatch.Success Then
If Not col.ContainsKey(name) Then
col.Add(name, hrefMatch.Groups(1).Value)
End If
End If
End If
End Sub
Another Method the Login function calls is the CreateRedirectUrl method. Again, pulling from the code in the download, we can create this method as follows:
Private Shared Function CreateRedirectUrl(_
ByVal requiredParameters As String, _
ByVal optionalParameters As String, _
ByVal delgate As String, _
ByVal identity As String) As String
Dim sb As New StringBuilder()
sb.Append("?openid.ns=" + HttpUtility.UrlEncode _
("http://specs.openid.net/auth/2.0"))
sb.Append("&openid.mode=checkid_setup")
sb.Append("&openid.identity=" + HttpUtility.UrlEncode(delgate))
sb.Append("&openid.claimed_id=" + HttpUtility.UrlEncode(identity))
sb.Append("&openid.return_to=" + HttpUtility.UrlEncode _
(HttpContext.Current.Request.Url.ToString()))
If Not String.IsNullOrEmpty(requiredParameters) OrElse _
Not String.IsNullOrEmpty(optionalParameters) Then
sb.Append("&openid.ns.sreg=" + HttpUtility.UrlEncode _
("http://openid.net/extensions/sreg/1.1"))
If Not String.IsNullOrEmpty(requiredParameters) Then
sb.Append("&openid.sreg.required=" + HttpUtility.UrlEncode _
(requiredParameters))
End If
If Not String.IsNullOrEmpty(optionalParameters) Then
sb.Append("&openid.sreg.optional=" & HttpUtility.UrlEncode _
(optionalParameters))
End If
End If
Return sb.ToString()
End Function
Finally, we have a number of places in the book (including our Login function) that use a datatype of OpenIdData. This class is also missing, but defined in the download code. To create this class, add the following definition below the OpenId class’ end class statement:
Public Class OpenIdData
Public Sub New(ByVal identity__1 As String)
Identity = identity__1
End Sub
Public IsSuccess As Boolean
Public Identity As String
Public Parameters As New NameValueCollection()
End Class
Adding Data to userProfile Control’s Country and State DropDownLists
On page 196, we find the code that we add a TextBox for State and a DropDownList for Country to our userProfile Control. But the interesting thing is that in the very same section, we define custom controls that inherit from the DropDownList control and will do all the populating for our controls. Why we go through the hassle of creating them if we don’t use them? I don’t know either..
The Missing GetProfile() Method of the userProfile Control
On page 201, we start loading the profile, this uses the GetProfile() function that isn’t defined in the book. This I also pulled from the code download:
Private Function GetProfile() As ProfileBase
Dim profile As ProfileBase = Helpers.GetUserProfile(_userName)
If IsNothing(profile) Then
profile = ProfileBase.Create(_userName, False)
End If
Return profile
End Function
You’ll notice that this function calls the Helpers.GetUserProfile function that also isn’t declared in the book. Again from the code download, we have the following overloaded functions:
Public Shared Function GetUserProfile() As ProfileBase
Return ProfileBase.Create(CurrentUserName, _
CurrentUser.Identity.IsAuthenticated)
End Function
Public Shared Function GetUserProfile(ByVal sUserName As String) _
As ProfileBase
Return ProfileBase.Create(sUserName, _
CurrentUser.Identity.IsAuthenticated)
End Function
Public Shared Function GetUserProfile(ByVal sUsername As String, _
ByVal isAuthenticated As Boolean) As ProfileBase
Return ProfileBase.Create(sUsername, isAuthenticated)
End Function
You’ll also notice towards the end of our Page_Load subroutine that our code will enter information for our Signature and our AvatarUrl. This isn’t added to the profile until a later chapter so doing it now will not work, but if you want to add the setting now, you can look it up on page 407, the chapter on forums. We run into this again when we attempt to save the signature back to the profile in the SaveProfile routine as well.
The Missing GetAvatarUrl() Method
I struggled a little trying to decide whether to put this here or into the Forum chapter, since it technically isn’t USED here. But I decided that since it was referenced here, I’d add it and it will be all setup for the forums chapter later. I did look in the forum chapter to see if it was defined there and I couldn’t find it, I had to go to the code download. So here is the GetAvatarUrl routine. This also contains the GetGravatarHash routine that is called by our GetAvatarUrl method (these both go into the userProfile control by the way):
Private Function GetAvatarUrl() As String
If String.IsNullOrEmpty(Profile.Forum.AvatarUrl) Then
Dim mu As MembershipUser = Membership.GetUser(_userName)
If Not IsNothing(mu) AndAlso Not String.IsNullOrEmpty(mu.Email) Then
Return String.Format _
("http://www.gravatar.com/avatar/{0}.jpg?d=wavatar&s=32", _
GetGravatarHash(mu.Email))
Else
Return String.Format("{0}/images/user.gif", Helpers.webroot())
End If
Else
Return Profile.Forum.AvatarUrl
End If
End Function
Public Function GetGravatarHash(ByVal sEmail As String) As String
Dim md5Hasher As MD5 = MD5.Create()
Dim data As Byte() = md5Hasher.ComputeHash _
(Encoding.Default.GetBytes(sEmail))
Dim sBuilder As New StringBuilder()
Dim i As Integer
For i = 0 To data.Length - 1
sBuilder.Append(data(i).ToString("x2"))
Next
Return sBuilder.ToString()
End Function
Registration Page - “Name Email not declared”
On page 207, 2nd paragraph, we learn that the “code-behind is impressively short” (and it is), however it is NOT true that we only have to add code to handle the FinishButtonClick event. We also need to add some properties or the code we created for the textbox on page 205 for the email textbox won’t work properly. In the book’s code, we add our text attribute, and add a value of ‘<%# Email %>’ . Email, however, is undefined. We must add the following (from the code download) to our code behind for the page to display correctly:
Public ReadOnly Property Email() As String
Get
Return Me.Request.QueryString("Email")
End Get
End Property
There are a number of other properties that are listed in the code download, however since I don’t know where they are used, I opted not to include them. I will add them later if required.
Password Recovery Page
On page 208, there is a slight error in the code, the page declaration sets the masterpage to “Template.master”, this according to the code download and other similiar pages should be the “CRMaster.master” page. There are a number of small code errors in this passage as well, like “<p></p>if you forgot your email…<p></p>” rather than “<p>if you forgot your email…</p>”. I’d submit it for errata, but I’ve had poor luck getting errors listed in the errata. All in all, this page seemed to be pretty well in order.
Location for the AJAX Login Dialog Stuff
On page 214, we’re introduced to an AJAX Login Dialog. While it mentions that this should be added to the bottom of the page. The page for which to add-this-to-the-bottom isn’t specified, however the code download shows this to be the TBHMain.Master page. The code should go between the end of the footer copyright Div section and the end </form> tag.
There does seem to be one typo that hasn’t been corrected, that is in the TextBox for the OpenID, there is an additional attribute that doesn’t belong there: ‘Type’. This doesn’t appear in the code download and should be be removed.
Also, on page 216, we’re introduced to some javascript code, which again is unclear where it goes. This is placed in an external .js file named TBH.js located in the scripts folder in our website project (although further code on the next page is referred to this .js file as though we should know it already exists…). Continuing the page, we also find reference to the ‘Client'-side’ functions, which are ALSO in the TBH.js file.
So, to sum up, all the javascript on pages 216-220 goes in this TBH.js file.
Code Corrections for the AJAX Login Dialog
While we’re at it, we might as well correct code in this section.
In the profile section, we’re missing a little code from the code download.
In the LoadProfile function, the code download also adds an array and populates it with the names of our properties, the following is before our call to Sys.Services…:
propertyNames[0] = "FirstName";
propertyNames[1] = "LastName";
In the OnLoadCompleted function, the last line before the closing bracket should call our ShowProfileInfo() function:
ShowProfileInfo();
Also, there is a type-o in the code for ShowProfileInfo(). In the nested If statement, there is a reference to ProfileEdit.aspx in one of our link tags, it should be EditProfile.aspx.
You may also notice that in our LoadProfile function, we pass a reference to OnProfileFailed, which isn’t in the book. The code download defines it as follows:
function OnProfileFailed(error_object, userContext, methodname) {
alert("Profile service failed with message: " +
error_object.get_message());
}
In the onLoadRolesCompleted function, the code download modifies the book’s code to a single line:
isAdministrator();
This runs a function that isn’t defined in the book, that checks the user’s roles to see if they are an administrator and then show’s hides certain functionality. The isAdministrator() function also calls ShowElement and HideElement. They should be defined as follows:
function isAdministrator() {
if (Sys.Services.RoleService.isUserInRole('Administrators')) {
ShowElement('liAdmin');
} else {
HideElement('liAdmin');
}
}
function ShowElement(ElementName) {
if (null != $get(ElementName))
$get(ElementName).style.display = 'block';
}
function HideElement(ElementName) {
if (null != $get(ElementName))
$get(ElementName).style.display = 'none';
}
Looking through the code download, I’d also add the following functionality that seems(?) relevant, but isn’t in the book:
function ValidateLoginValues() {
if (event.which || event.keyCode){
if ((event.which == 13) || (event.keyCode == 13)) {
Username = $get('txtUsername');
Password = $get('pwdPassword');
if (Username.value.length > 0 && Password.value.length > 0) {
ProcessEnter('btnLogin');
}
else {
alert('You must enter both a username and a password to authenticate.');
}
}
}
}
This function seems to disable the login button until the user has entered at least SOME text into both the username and password fields. It isn’t in the book, but I added it here.
Also, whether or not it all works, I don’t know, I’m not planning on testing the AJAX login stuff.
Code Corrections for Persisting the Theme
The code we previously created for the theme application should change a little here to use the profile’s theme setting. The code in the book passes a profile variable into Helpers.SetProfileTheme() however this is undefined. We can retrieve this using a function in the Helpers class GetUserProfile. Change the Helpers.SetProfileTheme as so it reads:
Helpers.SetProfileTheme(Helpers.GetUserProfile(), Me.Theme)
We pull the profile and then pass it into the class’ method. We also need to modify the line that retrieves the theme. Change your Helpers.GetProfileThem so it reads:
Me.Theme = Helpers.GetProfileTheme(Helpers.GetUserProfile())
Location for Profile_MigrateAnonymous
On page 221, we add handling for Profile’s MigrateAnonymous event. The book is unclear that this should go into our Global.asax file (oh, and you may have to create it. It goes in the root of you web directory).
Location for the AdminMenuItems Class
On page 222, we’re introduced to the AdminMenuItems class, that we’ll be using as we create the admin menu. This should be placed in the Navigation folder in the BLL portion of the project.
The AdminMenuItem Class (singular)
Also on page 222, it mentions that we’ll call the AdminMenuItems class (plural) and that will give us an arraylist of AdminMenuItem (singular). This (the singular) isn’t defined in the book, so we’ll define it here. This is placed in the AdminMenuItems.vb class file after the AdminMenuItems class ends. It’s actually pretty simple, it is defined as follows:
Public Class AdminMenuItem
Public Sub New(ByVal vName As String, ByVal vUrl As String)
MenuName = vName
Url = vUrl
End Sub
Public Sub New(ByVal vName As String, ByVal vImageUrl As String, _
ByVal vUrl As String)
MenuName = vName
Url = vUrl
ImageUrl = vImageUrl
End Sub
Private _name As String
Public Property MenuName() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Private _imageUrl As String
Public Property ImageUrl() As String
Get
Return _imageUrl
End Get
Set(ByVal value As String)
_imageUrl = value
End Set
End Property
Private _url As String
Public Property Url() As String
Get
Return _url
End Get
Set(ByVal value As String)
_url = value
End Set
End Property
End Class
Location for BindNavItems
This should be added to the code behind for the Admin.master page.
Guts of the ManageUsers page
I didn’t want to recreate the wheel, so I simply used the ‘Better ASP.Net Member/Role Management’ control that I build (see part 9 here for details) instead. I did use the stuff about total users registered, and users online now, and then added my control under that. I did modify the binding code some, in the book it calls BindAlphabet(), I changed my to BindTotals() and all it does is enter values into the lblTotalUsers and lblOnlineUsers controls. I removed all the alphabet stuff.
Interestingly enough, the book has two code listings for BindUsers(). I believe that the second one is more accurate, as the first calls the BindAlphabet() method which I don’t believe it should.
AddEditUsers page
Looking at the code for the AddEditUser page as well, I believe the ‘Better ASP.Net Member/Role Management’ control can handle all this as well. I DO miss a little of the statistical information such as when the user last logged in and last activity etc. AND I do miss the section that allows the administrator to edit the user’s profile. Also, I don’t provide a means of unlocking the user’s account if it is locked. Maybe I’ll look into a 10th part that adds a way to handle these missing pieces?
I did find on page 239, there seems to be an error in the text that give us the list<string> definition in C# rather than in VB (and the entire explanation is a little hard to follow, since by this point you ought to be able to do list(of String) variables). Again, I’d submit to errata, but it doesn’t seem to be very helpful.
ManageRoles Page
Again, the “Better ASP.Net Member/Role Management” control can handle all the role management I need, so I chose to ditch this page as well.
AddEditRole Page
Again, this functionality is addressed in the control, however I thought to comment on one piece of code the last one on page 245. Specifically, I might have done it a little differently. Instead of using a string collection (Usernames) and a string array (saUsers) I would have used a list(of String) generic collection, and then iterated through the MembershipCollection returned form Membership.FindUsersByName() and added the usernames to the list via the list.add method. Once the list is complete, I would have returned list.ToArray() – That’s what I’m doing in another project I’m working on.
Also, the book doesn’t delineate where this function should go. In the code download, you can see find it in the App_Code folder in a file called MemberService.vb.
Epilogue
I did make a list as I went through some of the last bits of this site, and found some functionality that I will add to my user/role management control. I did find however that there was a lot of pieces in this chapter that created in the book, but never seem to be quite tied in (i.e. ajax login and openid login).
I did like the suggestion to use a UserProfile control, and have incorporated the idea into a project I am currently working on, and even referenced the code in the book for ideas on how to code it.
I hope that some of what’s here can make things more clear. I apologize for being so long between posts on this topic, and likely it’ll be long again. I have read some in chapter 5 on newsletters etc and have a list of things to start posting about, but that doesn’t mean I’ll get to it any faster, but I’ll do what I can.