Welcome to AspAdvice Sign in | Join | Help

.Net Discoveries

An attempt to pass along some answers I have discovered in my .Net coding.
Tracking Downloads using Google Analytics

Prologue

Google Analytics is an awesome way to track the happening of our website and the metrics we get from Analytics provides give us with critical insight into what our website visitors are doing once they visit our site. Google Analytics does have its shortcomings though, one being that by default, PDF downloads (and other documents) are NOT tracked. But there are ways to work around this.

Problem

On our company website, we have a document library. In this library, we have a multitude of PDF documents. In addition, we have a number of other locations that also contain PDF documents that we would like to track. If I understand it right, there are built-in solutions that you can use with Google Analytics like tagging that may help, but we didn’t have the time to research this avenue and to figure out how to use it (the website isn’t my primary function).

Several years ago, we solved the problem by finding a javascript solution by Stephane Hamel based on work by Justin Cutroni. This worked for quite a while, however the new Universal Analytics upgrade to Google Analytics broke it. Long story short, in this post, we’ll look at a solution that works with the current (as of July 2014) version of Google’s Universal Analytics to track downloads. A bunch of credit goes to the previously mentioned Hamel code, which has probably been changed nearly out of recognition. I’ll also point out a couple gotchas that we experienced in our initial implementation.

Solution

This solution is a javascript file so we don’t really need to create any asp.net, but we’ll need to have a page to test our javascript on. I’m not going to go into creating a test page, I’ll just go through this javascript page. So, let’s start with a new script file, I’ll call it gaUATrackDownloads.js.

First we’ll start with some configuration settings that will make it easier to change any of our settings without having to change the actual code. Add the following to your file:

//Configuration Section
//Should these events be tracked at all?
var bDoTrackOutbound = false; //set to true to track outbound events
var bDoTrackDownloads = true; //set to true to track download events
var bDoTrackViews = true; //set to true to track view events

//Should it be tracked as an event or pageview
var bUseEventForOutbound = true; // Set to false to use pageview for outbound links (if bDoTrackOutbound = true)
var bUseEventForDownload = true; // Set to false to use pageview for downloads (if bDoTrackDownloads = true)
var bUseEventForView = true; // Set to false to use pageview for downloads (if bDoTrackViews = true)

// Indicate each file extension that needs to be tracked,
//  downloadTypes is the regular expression that matches downloadable files,
//  viewTypes is the reg expr that matches viewed images
var downloadTypes = new RegExp(/\.(docx*|doc|xlsx*|pptx*|zip|pdf|jpg|csv)$/i);
var viewTypes = new RegExp(/\.(gif|png|jpg)$/i);

//END Configuration Section

Most of the settings should be pretty self explanatory, should we track events at all, should we track them as page views or events? We chose to track downloads as events. Finally, we have some settings to specify what we actually want to track. It may be that we don’t want to track certain types of documents, but we want to make sure that we track others, this is where we can do that. The above is our actual list. We track all Microsoft documents that we have on our site as well as several other types (csv, pdf, zip).

Next, we will setup some event listening for when people click on links that match our tracking criteria above. Add the following below your configuration section:

/// No need to change anything below this line
// Initialize external link and download handlers
function domReady(){
    if (document.getElementsByTagName) {
        var hrefs = document.getElementsByTagName('a');
        // alert(hrefs.length); //uncomment to trouble shoot.
        for (var l = 0, m = hrefs.length; l < m; l++) {
            if (hrefs[l].hostname == location.host) {
                if (bDoTrackDownloads == true && downloadTypes.test(hrefs[l].pathname)) startListening(hrefs[l], 'click', trackDownloads);
                if (bDoTrackViews == true && viewTypes.test(hrefs[l].pathname)) startListening(hrefs[1], 'click', trackViews);
            }
            else {
                if (bDoTrackOutbound == true) startListening(hrefs[l], 'click', trackExternalLinks);
            }
        } // end for
    }
}

/* This section will setup the non jQuery equiv. of Document.Ready, when DOM is ready, it will call the domReady Function*/
// Mozilla, Opera, Webkit
    if (document.addEventListener) {
        document.addEventListener("DOMContentLoaded", function () {
            document.removeEventListener("DOMContentLoaded", arguments.callee, false);
            domReady();
        }, false); 

        // If IE event model is used
    } else if (document.attachEvent) {
        // ensure firing before onload
        document.attachEvent("onreadystatechange", function () {
            if (document.readyState === "complete") {
                document.detachEvent("onreadystatechange", arguments.callee);
                domReady();
            }
        });
    }
/* end DOM ready setup section */

You’ll notice in our domReady function that we search our page for ‘a’ tags, cycle through them, check if their link ends in one of our tracking types and if so, then add an event listener for the click event. Originally, this code was NOT in the domReady function, and thus it ran immediately when it was parsed. The problem ended up that when you put his script into the head section of your page, it runs before the DOM has been built, and no ‘a’ tags exist so none of the links were registered and clicking them did not record any downloads. Now we wait until the DOM has been fully built and then run the domReady function. The section below the domReady function simulates the jQuery document.ready function, but doesn’t require that we have jQuery loaded. When the DOM is fully loaded, then we’ll call domReady() and parse our page.

Finally, we’ll add the functions that will actually record the events. Add the following to your script file:

function startListening(obj, evnt, func) {
    if (obj.addEventListener)
        obj.addEventListener(evnt, func, false);
    else
        if (obj.attachEvent)
            obj.attachEvent("on" + evnt, func);
}

// The ga object listed below should be created by the Google Analytics code itself, we can use it in this script if
//  it has been created correctly.
function trackDownloads(evnt) {
    if (typeof (ga) != "function")
        return;
    bUseEventForDownload ? ga('send', 'event', 'Download', 'Download', (evnt.srcElement) ? Normalize(evnt.srcElement.pathname) : Normalize(this.pathname), { 'nonInteraction': 1 }) : ga('send', 'pageview', (evnt.srcElement) ? evnt.srcElement.pathname : this.pathname);
}

function trackViews(evnt) {
    if (typeof (ga) != "function")
        return;
    bUseEventForDownload ? ga('send', 'event', 'View', 'View', (evnt.srcElement) ? Normalize(evnt.srcElement.pathname) : Normalize(this.pathname), { 'nonInteraction': 1 }) : ga('send', 'pageview', (evnt.srcElement) ? evnt.srcElement.pathname : this.pathname);
}

function trackExternalLinks(evnt) {
    if (typeof (ga) != "function")
        return;
    var elmnt = evnt.srcElement;
    if (elmnt) {
        while (elmnt.tagName != "A")
            elmnt = elmnt.parentNode;
        bUseEventForOutbound ? ga('send', 'event', 'outbound', 'click', elmnt.hostname + '/' + elmnt.pathname + elmnt.search, {'nonInteraction': 1}) : ga('send','pageview', elmnt.hostname + "/" + elmnt.pathname + elmnt.search);
    }
    else
        bUseEventForOutbound ? ga('send', 'event', 'outbound', 'click', this.hostname + this.pathname + this.search, {'nonInteraction': 1}) : ga('send','pageview', this.hostname + this.pathname + this.search);
}

//for downloads, some show with leading / and some don't. Remove this leading / if it exists to promote greater normalization.
function Normalize(sStringIn) {
    return (sStringIn.indexOf('/') == 0 ? sStringIn.substring(1) : sStringIn);
}

Here’s the break down. The startListening function will add the event listener to our elements as called by our above mentioned functions. Each of the trackXYZ functions will call the Google Analytics code and record the event. You’ll notice that each call to the Google code has some parameters that can be set for classifying things (like download or view) those can be changed, if you want to get more specific, we didn’t.

Finally, I have a Normalize function that takes off any leading slash. I found that we had a mixture of results, with and without the leading slash and this function will correct this so that the same downloads will all get counted as one item rather than splitting over two.

Epilogue

There it is. All we needed to track any of our file downloads. We now have a clean reading in Google Analytics regarding what is getting downloaded and how often. Thanks again to those listed above who started the process of creating this file, much appreciated.

The Convenience of Using Extension Methods

Prologue

Extension methods in .Net are nothing new, they have been around since at least Visual Studio 2008. I finally got around to learning how to use them and have found them incredibly easy to create, and very convenient to use.

Problem

For this exercise, we’ll keep it pretty simple, we’ll add two extension methods, one for strings, and one more complex so that we can see how (and that its possible) to use them in both instances. Creating extension methods affords us a couple conveniences:

  • We can extend types that we can’t control (such as string) without having to write a new class that derives from it. This also applies to complex types (classes) that we don’t have control over.
  • The code becomes much more simple and cleaner than adding helper functions to our code just to do a simple task, AND the extension will be available throughout our project rather than just in the smaller scope where we create our helper function.
  • Calling the extension method is much cleaner than calling a helper function in many instances as well.

Solution

For this solution, we’ll use ASP.Net and create a new site. Start with a page titled Test.aspx. Next, add a class module to your project and let’s call it Extensions.vb. Let’s add our “complex” class to the class module, add the following to your Extensions.vb class:

Public Class Toaster
    Public Property Doneness As Integer = 5
    Public Property Make As String = ""
    Public Property Model As String = ""
    Public Property PurchaseDate As DateTime = Nothing 

    Public Sub New(sMake As String, sModel As String, _
                             dPurchaseDate As DateTime)
        Make = sMake
        Model = sModel
        PurchaseDate = dPurchaseDate
    End Sub
End Class

On our Test.aspx page, lets add one TextBox and name it txtTheText as well as two buttons, one labeled btnString and one labeled btnComplex and a label for results with the id of lblResults.

Now we can get started, let’s start with our string. Let’s add an extension to Capitalize our string’s first character. We could do this by creating a helper function and then calling it in our Test.aspx page:

Private Function Capitalize(sInput As String) As String
    Return sInput(0).ToString().ToUpper() + sInput.Substring(1)
End Function

Protected Sub btnString_Click(sender As Object, e As System.EventArgs) _
    Handles btnString.Click

    lblResults.Text = Capitalize(txtInput.Text)
End Sub

This will successfully do what we’re looking for, however we have to pass our string into the function, it would be cleaner to call out our string as txtInput.Text.Capitalize(). In addition, if we want this available on other pages, we either have to copy and paste the code over and over, or create a page with helper functions and still pass in our string. Let’s convert it to an extension method and clean it up some. Delete your Capitalize method and let’s go over to our Extensions.vb code. Add the following to your code there, make sure it is OUTSIDE of your Toaster class:

Imports System.Runtime.CompilerServices

Public Module ExtensionsToAdd 

   <Extension()> _
   Public Function Capitalize(sInput As String) As String
      Return sInput(0).ToString().ToUpper() + sInput.Substring(1)
   End Function

End Module

Now we can call it just the same as we previously did:

lblResults.Text = Capitalize(txtInput.Text)

or we can use it as though it were a part of the string class:

lblResults.Text = txtInput.Text.Capitalize()

Very Nice! Try it out, it works and it works pretty easily. Notice a couple of things:

  1. Our extensions have to go in a module, not in a class.
  2. Each of our extensions have to have the <Extension()> _ decoration before you define the extension.
  3. You’ll need to either import the System.Runtime.CompilerServices namespace, or your decoration will need to include that namespace before Extension i.e. <System.Runtime.CompilerServices.Extension()> _.
  4. Notice that in the second method of calling our extension method, we don’t actually pass in anything, and that our method has a parameter. The string that we use to call the method will automatically be passed in as the parameter. Thus, we can actually use ANY string and call the method: “mystring”.Capitalize() will work just as well.

That’s how easy it is to do an extension method. Let’s do one for our Toaster class. This is pretty easy as well, let’s create one that will return the make, model and purchase date as a string. Create a new extension method in your module as follows:

<Extension()> _
Public Function FullTextString(value As Toaster) As String
    Return value.Make & " " & value.Model & " " & value.PurchaseDate
End Function

Then we can use it in our code as follows:

Protected Sub btnCopmlex_Click(sender As Object, e As System.EventArgs) _
        Handles btnCopmlex.Click

    Dim objToaster As New Toaster("Rubbermaid", "Toastmaster",  _
                  DateTime.Now())
    lblResults.Text = objToaster.FullTextString()
End Sub

Test it, and you’ll see that it works just fine.

You might be asking, why don’t I just create a new method for my class? And that’s a good question. There are a couple reasons why an extension method might make more sense here’s one: what if it isn’t your class? If you can’t modify the class to add the method, this is a good way to extend it’s functionality (For example, we’re extending the String class which we can’t modify), which means that if we don’t control it, we don’t have to create a new class that inherits from the one you can’t manipulate.

Epilogue

And there it is, a quick and easy way to add functionality that is easy to reuse, and easy to use. It’s hard to believe I’ve been holding out to learn these for this long.

Setting Focus to a Control in a ModalPopupExtender

Prologue

I’ve found that we quite often use the ModalPopupExtender included in the Ajax Control Toolkit on our internal intranet site. We have a number of places where users have requested that since they use it so often that we have the cursor go to a specific field when the popup is displayed. This isn’t quite as straightforward for us as it sounds.

Problem

Since the ModalPopupExtender is a client-side code piece, it isn’t as straightforward as it sounds to put code in the set the control focus. While our site usually uses an UpdatePanel so that we can do a partial postback and do some processing and then display the ModalPopup from the code-behind, we can’t just use the .Focus() method when calling a ModalPopup, for us it doesn’t work.

I’ve tested several different things in other smaller applications and found that it seems to not be an issue on smaller lightweight pages, however, our site has some rather complex ModalPopups and they are on rather complex pages. For some reason we can’t just use the .Focus() method of the textbox and have it work. Our issue seems to be a slight delay in processing, basically the .Focus() method get’s called/run before the ModalPopup is ready and so focus isn’t given.

In this post, we’ll look put together a simple workable JavaScript solution that is written all in code-behind.

Solution

To get started, let’s create a small page with a Panel and ModalPopupExtender. We’ll also create a LinkButton to show the ModalPopup. Create a new .aspx page with the following HTML code:

<ajaxToolkit:ToolkitScriptManager ID="atScriptManager" runat="server" /> 
   <asp:UpdatePanel ID="upnlEverything" runat="server">
      <ContentTemplate>
        <asp:LinkButton ID="lnkbtnShowModal" runat="server"
                Text="Add Something" />
        <asp:Panel ID="pnlTest" runat="server" BorderColor="Red"
                      BorderStyle="Solid" BorderWidth="2">
            <asp:TextBox ID="txtTest" runat="server" /><br />
            <asp:LinkButton ID="lnkbtnClose" runat="server" Text="Close" />
        </asp:Panel>
        <asp:Button ID="btnFakeMPEButton" runat="server"
                      style="display:none;" />
        <ajaxToolkit:ModalPopupExtender ID="mpeThePopup" runat="server"
            TargetControlID="btnFakeMPEButton"
            PopupControlID="pnlTest" />
    </ContentTemplate>
</asp:UpdatePanel>

Note: I have previously registered my Ajax Control Toolkit in the Web.config so that I don’t have to explicitly register it on each page, and I created my own prefix for it (ajaxToolkit).

The code is pretty straight forward, I have created a LinkButton (lnkbtnShowModal) that I’ll use to display the ModalPopup from the code-behind. The ModalPopupExtender properties can be set to acknowledge the link and skip the code-behind piece, however we usually do some processing behind the scenes before we display the ModalPopup so I usually create a fake button (btnFakeMPEButton) that I feed the ModalPopupExtender since it won’t run without one.

In the panel we’re going to be extending, there’s one textbox (txtTest) and one LinkButton for closing the dialog (lnkbtnClose). Our object will be to set the focus to the text box when we display the ModalPopup. I’ve also added a little formatting to the panel just so that it’s more visible on our page.

Finally, we’ll set up a little back-end code so that when we click lnkbtnShowModal, the ModalPopup will be displayed. In the code-behind, add the following line to lnkbtnShowModal’s Click event handler:

mpeThePopup.Show()

Currently, if we run our application, we’ll see that the link will open the ModalPopup, but that the textbox is NOT set to focus. Since our code-behind gives us the ability to set focus to controls, it seems like this would be a good way to go. But this it isn’t working for us, so we’ve created some JavaScript to do it. Let’s create a new subroutine in our code-behind and add some JavaScript to our page as it is rendered.

Private Sub AddJavaScript()
    Dim scriptBlock As New StringBuilder() 

    scriptBlock.Append("<script type=""text/javascript"">" & vbCrLf)
    scriptBlock.Append("    Sys.Application.add_load(function() {" & vbCrLf)
    scriptBlock.Append("        var thePopup = $find('" & _
                   mpeThePopup.ClientID & "');" & vbCrLf)
    scriptBlock.Append("        thePopup.add_shown( _
                   function() {popupFocusTimer();});" & vbCrLf) 

    scriptBlock.Append("        function popupFocusTimer() _
             { setTimeout(popupFocus,300); }" & vbCrLf)
    scriptBlock.Append("        function popupFocus() _
     { document.getElementById('" & txtTest.ClientID & "').focus();}" & vbCrLf)
    scriptBlock.Append("   });" & vbCrLf) 

    scriptBlock.Append("</script>" & vbCrLf)
    If (Not Page.ClientScript.IsStartupScriptRegistered("setFocus")) Then
        Page.ClientScript.RegisterStartupScript(Me.GetType(), "setFocus", scriptBlock.ToString())
    End If
End Sub

This subroutine will create a script block and add the appropriate JavaScript code to set our focus. We found in our environment that we had to add a slight delay before setting focus, so when the link is clicked, we actually call setTimeout and pass in the function that does the actual focus call to the setTimeout function.

The Sys.Application.add_load() call basically runs this enclosed function when the page is finished loading. This ensures that everything is on the page and ready before the code is run. Within the Sys.Application function, we retrieve the ModalPopup as an object and add an event handler for when it is shown. The add_shown event handler is triggered when the dialog is shown. We’ll listen for this event and then we’ll start the timer function.

The next section creates two functions. One that sets a timer for .3 seconds and then calls the other, which actually does the call to the JavaScript focus() method. Notice that since we’re in the code-behind, we can easily retrieve the control’s clientID and enter it into the code.

In the final section, we add this code to the page by using the Page.ClientScript.IsStartupScriptRegistered method. This allows us to help keep from getting the code added more than once and giving us errors for multiple functions name the same etc.

Finally, add the following line to your Page_Load event handler to run our AddJavaScript subroutine:

AddJavaScript()

Now if you run your application, when you click the lnkbtnShowModal link, you’ll see that the Textbox receives the focus.

Epilogue

While many people will find it no problem to use the .Focus() method in the code-behind, we’ve found it problematic in our environment due to page complexity and load times. This has been a simple yet effective way for us to work around that issue.

Custom Configuration Sections in Web.Config

Prologue

Ever since I read my Asp.Net Problem-Design-Solution 2.0 book several years ago, I’ve been hooked on using custom configuration sections and then putting all the configuration settings into my web.config file. This keeps all the settings in one central location and makes it easy retrieve settings or to make updates or changes to them later.

Problem

Putting all your settings into the web.config is an easy way to get all your settings into one central location. It makes updating the settings very easy and means that they aren’t buried inside some code somewhere. This also means that I don’t have to recompile any files before settings take effect. By creating a custom class to access these settings, they also become very easy to retrieve and I don’t have to remember any specific string literals to pull from web.config so the number of times that I fat-finger something is reduced significantly.

In this post we’ll create a custom configuration class that allows us to pull custom setting information from our web.config easily. These custom settings we’ll put into a custom section.

Let’s start by defining what we want our configuration file to look to like. Once we have the basics, additional settings can be added to it without much trouble. Our config section will start looking like this:

<MyConfigurationSection>
    <images location=”~/images” maxWidth=”100” />
    <caching enabled=”true” cacheLength=”10” />
</MyConfigurationSection>

We're going to create an images section with a couple settings for images and a cache section, also with just a couple settings.

Solution

Create a new ASP.Net solution. We’ll need to add the App_Code folder (right-click the solution and select ‘Add ASP.Net Folder’ and select the ‘App_Code folder). In this folder, add a new class, we’ll call it configuration.vb. This will house all our code to facilitate the retrieval of the configuration information from the web.config.

We’re going to modify our Configuration class declaration a little, we want it to inherit from the ConfigurationSection object. In addition, we want to call it something a little less generic so that it stands out in the web.config and in our code. Modify your class declaration so it reads as follows:

Public Class MyConfigurationSection
    Inherits ConfigurationSection

End Class

We are also going to add another class to this module so that we can access this particular class anywhere in our application. This won’t be a long class so we’ll put it just before the MyConfiguration class. We’ll also need to add a reference to the System.Web.Configuration namespace. It should be defined as follows:

Imports System.Web.Configuration

Public NotInheritable Class MyConfigurations
    Public Shared ReadOnly Settings As MyConfigurationSection = _ 
          CType(WebConfigurationManager.GetSection _
          ("MyConfigurationSection"), MyConfigurationSection)
End Class

Basically this will create a shared class that we can use to pull our settings very easily in our code. We’re retrieving the configuration section from the web.config and forcing it into an object of type MyConfigurationSection.

Now we’re ready to start creating configuration sections. Let’s create our Images subsection. We do that by creating a new class for our Images section and then creating a property in our main class that returns our images class. Inside our MyConfigurationSection class, add a class definition as follows:

Public Class ImagesElement
    Inherits ConfigurationElement

End Class

Notice that our class will need to inherit the ConfigurationElement object. Now within the ImagesElement class, add the following properties:

<ConfigurationProperty("location", IsRequired:=True)> _
Public Property Location() As String
    Get
        Return CStr(Me("location"))
    End Get
    Set(value As String)
        Me("location") = value
    End Set
End Property

<ConfigurationProperty("maxWidth", DefaultValue:="100")> _
Public Property MaxWidth() As Integer
    Get
        Return CInt(Me("maxWidth"))
    End Get
    Set(value As Integer)
        Me("maxWidth") = value
    End Set
End Property

There are a couple of things to note with these properties:

  1. They have the ConfigurationProperty declaration added to each of them.
  2. We can add other modifiers such as IsRequired or DefaultValue to the declaration to configure them.
  3. Since we’re pulling from the web.config, and the web.config is XML, capitalization counts for all string literals, even if it doesn’t in the language you’re programming in (VB’ers, take notice). Notice that when we get/set Me(“xyz”) that the XYZ has to exactly match what is in the web.config including case.
  4. To keep with casing standards for XML, all our actual web.config settings will be lowercase then uppercase for subsequent words (i.e. maxWidth).
  5. Our actual property has an initial uppercase (i.e. MaxWidth) as this does NOT have to conform to XML standards.

Now, we need to add a property to our MyConfigurationSection class that returns the ImagesElement. To do this add the following to your MyConfigurationSection class:

<ConfigurationProperty("images", IsRequired:=True)> _
Public ReadOnly Property Images As ImagesElement
    Get
        Return CType(Me("images"), ImagesElement)
    End Get
End Property

Notice that again, we’re using the ConfiguartionProperty declaration, however this property is read only. We won’t be setting the entire element as a whole, just the individual settings which are at a lower level in the object hierarchy.

Now, you can test how it works by adding the MyConfigurationSection and the images sub section from above to your web.config. We’ll need to define a custom section and then add our custom section. We’ll do this right at the very top just below the open <configuration> tag. Add the following:

<configSections>
    <section name="MyConfigurationSection"
                 type="MyConfigurationSection" />
</configSections>
<MyConfigurationSection>
    <images location="~/images" maxWidth="100" />
</MyConfigurationSection>

We first instruct the web.config to know that we’re adding a special configuration section and what it’s type is so it knows how to handle it. We then can enter the custom configuration section.

Add a page to our project, add a label to that page named lblResults and add the following to your page_load event handler and watch it work:

lblResults.Text = MyConfigurations.Settings.Images.Location

Let’s add our other section for caching. Go back to your configuration.vb file and let’s add another class to contain our caching elements. Add the following after the end of our ImagesElement class but before the end of our MyConfigurationSection class:

Public Class CachingElement
    Inherits ConfigurationElement 

    <ConfigurationProperty("enabled", DefaultValue:="True")> _
    Public Property Enabled As Boolean
        Get
            Return CBool(Me("enabled"))
        End Get
        Set(value As Boolean)
            Me("enabled") = value
        End Set
    End Property 

    <ConfigurationProperty("cacheLength", DefaultValue:="10")> _
    Public Property CacheLength As Integer
        Get
            Return CInt(Me("cacheLength"))
        End Get
        Set(value As Integer)
            Me("cacheLength") = value
        End Set
    End Property
End Class

We’ll also add a property to our MyConfigurationSection class to access this new CachingElement class. Add the following just below our read-only Images property in the MyConfigurationSection class:

<ConfigurationProperty("caching", IsRequired:=True)> _
Public ReadOnly Property Caching As CachingElement
    Get
        Return CType(Me("caching"), CachingElement)
    End Get
End Property

Finally, we can add our caching section to the web.config. Add the following inside our MyConfigurationSection section:

<caching enabled="true" cacheLength="10" />

Getting a little more complicated

Now let’s say we want to add settings that are a little more complicated. Let’s add another section as follows to our MyConfigurationSection:

<email>
   <add name=”John” address=”xyz@something.com” />
   <add name=”Peggy” address=”123@somethingelse.com” />
</email>

This is a little more complicated since we’ve now got several elements to return rather than a single attribute.

We’ll start much the same way that we did above, we’ll create a class that defines each element (the add element), we can define that as follows and add it after the CachingElement but before the end of our MyConfigurationSection class:

Public Class EmailElement
    Inherits ConfigurationElement 

    <ConfigurationProperty("name", IsRequired:=True, IsKey:=True)> _
    Public Property Name As String
        Get
            Return CStr(Me("name"))
        End Get
        Set(value As String)
            Me("name") = value
        End Set
    End Property 

   <ConfigurationProperty("address", IsRequired:=True)> _
    Public Property Address As String
        Get
            Return CStr(Me("address"))
        End Get
        Set(value As String)
            Me("address") = value
        End Set
    End Property
End Class

Notice there is a slight difference in the Name property’s declaration, IsKey. Since we’ll have a collection, we need a way to identify each item in the collection uniquely, this will be our Key. We’ll code against this in a moment.

Next, we need to add a class that will return a collection of our EmailElement class. Notice that this inherits from ConfigurationElementCollection rather than just the ConfigurationElement. When we add it’s inheritance, we’ll be given stubs for two methods that we need to define for this class, CreateNewElement and GetElementKey. Add the following just below our EmailElement:

Public Class EmailCollectionElement
    Inherits ConfigurationElementCollection 

    Protected Overloads Overrides Function CreateNewElement() As  _
                                             System.Configuration.ConfigurationElement

        Return New EmailElement
    End Function 

    Protected Overrides Function GetElementKey(_
               element As System.Configuration.ConfigurationElement) As Object

        Return CType(element, EmailElement).Name
    End Function
End Class

For the most part, they are really simple, we’re just adding specifics for our classes. In CreateNewElement, we simple return a new EmailElement. In our GetElementKey, we simply take the Element that was passed in, cast it to EmailElement and then return the name property. We’ll also need to add an Item method so that we can retrieve elements from within our code. Add the following method to your EmailCollectionElement class:

Default Public Shadows ReadOnly Property Item(name As String) _
                                                                          As EmailElement
    Get
        Return CType(BaseGet(name), EmailElement)
    End Get
End Property

Finally, we need to add a property to our MyConfigurationsSection to access our collection. Directly below our Caching property, add the following:

<ConfigurationProperty("email", IsRequired:=True)> _
Public ReadOnly Property Emails As EmailCollectionElement
    Get
        Return CType(Me("email"), EmailCollectionElement)
    End Get
End Property

Now we can use this in our code to retrieve the settings. We have several ways we could reference the items, I’ll show two. First we can retrieve a singular item. Modify your TestConfig.aspx.vb so the page_load reads as follows:

lblResults.Text =  MyConfigurations.Settings.Emails("John").Address

If you run your application, you’ll notice that we successfully retrieve the address from the “John” element.

Next, we can iterate through the elements. Change the Page_Load to read as follows:

Dim sTemp As String = ""
For Each item As MyConfigurationSection.EmailElement In _
                                                  MyConfigurations.Settings.Emails

    sTemp &= item.Name & " - " & item.Address & "<br>"
Next
lblResults.Text = sTemp

This will successfully iterate through our collection and parse the results. You will notice however, that we cannot pull a specific element using it’s ordinal position (i.e. MyConfigurations.Settings.Emails(0).Address). The ConfigurationElementCollection doesn’t support indexing in this manner.

Epilogue

For our uses, this has been an extremely effective way to keep settings all together in a place that doesn’t require any kind of recompiling for the changes to take effect. It also provides a very easy way to access the settings via code without the worry of fat fingering some string literal somewhere. We’ve used it very effectively and extensively in our code.

I’m sold, maybe you will be too…

Using ASP.Net to Create JavaScript Code

Prologue

Several years ago, I created a post on this blog about using ASP.Net to create JavaScript variables. Times have changed, and I’ve found that sometimes I need to add more than just a couple variables to JavaScript to the page. Luckily, I found an easy way to add JavaScript code to your page ALL in the backend so that you don’t have to use those messy HTML/ASP.Net pieces.

Problem

When I started coding one of the most desired features of our Intranet, I ran into a slight issue with how long the page took to load. The page has over 100 data fields on it, and several of them will be displayed only if a certain option is selected from a DropDownList. While this isn’t hard to do, I was using a postback (with the UpdatePanel) to go back to the server and determine if the field needed shown and validation enabled for it.

The problem with this was that since the page was so hefty, it was a several second delay as it reprocessed the page. Users complained. I could use JavaScript to do the showing/enabling, however it’s typically a pain to identify the elements that are created by the ASP.Net code since it gives them lengthy ID’s at runtime. Thus the previous post about adding in variables to the JavaScript code so that the ClientID can be injected.

There is an easierbetter(?) way. You can create your entire JavaScript code block in your back-end code and then push it to the page. I found this also comes in quite handy if there are blocks of JS code that you may or may not want to be put to the page depending on options that are selected, thus possibly cutting down on page load time.

Adding a JavaScript code block isn’t all that difficult, there are several articles regarding in online, but I found it handy enough that I thought I’d post another.

Solution

Let’s get started. Open a new page in Visual Studio. We’ll create some working JavaScript on the page, and then convert it to code-behind. I’m going to make use of jQuery a little so I will be adding a script tag that references the jQuery framework to the page:

<script src="../Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>

Add in HTML input element as follows:

<input type="text" id="txtTheOldBox" />
<input type="button" id="btnOld" onclick="countIt()" value="Count It" />

Let’s add a script section to your page with the following BLOCKED SCRIPT

<script type="text/javascript">
    function countIt()
    {
        alert($(‘#txtTheOldBox’).attr('value').length);
    }
</script>

Basically we’re just having the click event of the button alert us about the length of the text in the textbox, nothing overly special. Run and test it, and make sure it works.

Now, let’s add an ASP.Net textbox and make it a little more difficult. Add the following just below your input element:

<br /><asp:TextBox ID="txtTheBox" runat="server"  />
<asp:Button ID="btnNew" runat="server" OnClientClick="countIt()"
        Text="Count It" />

Now the question becomes, how do we identify our textbox in our JavaScript code since .Net will assign it a different id than the one we specify? In the previous post, I put embedded ASP.Net code into the JavaScript and when the page was parsed, the variables were entered. This gets a little problematic as it doesn’t always work cleanly. I’ve taken to putting this script into the code-behind and making it a little easier.

Let’s move our current JavaScript to our code-behind. First, let’s setup our Page_Load event so that it will call an AddJavaScript subroutine that will take care of adding our JavaScript. Add the following line to your Page_Load event handler:

AddJavascript()

and then start a subroutine for our Javascript as follows:

Private Sub AddJavaScript()
    Dim scriptBlock As New StringBuilder() 

    scriptBlock.Append("<script type=""text/javascript"">" & vbCrLf) 

    scriptBlock.Append("</script>") 

    If (Not Page.ClientScript.IsStartupScriptRegistered("theScript")) Then
        Page.ClientScript.RegisterStartupScript(Me.GetType(), _
                   "theScript", scriptBlock.ToString())
    End If
End Sub

This is the basic start of our AddJavascript subroutine. We’ll create a StringBuilder object and then we’ll add our script block open and close to the StringBuilder. Finally, we’ll check and see if we have added this script to our page before. If we haven’t, then we’ll add the script to the page using the Page.ClientScript.RegisterStartupScript function. Notice that we pass in a name for the script (“theScript”) this is what we check against to make sure that we don’t add it twice and cause any problems.

Now, we’ll just want to start adding our JavaScript code between the two script tags. Add the following:

scriptBlock.Append("    function countIt()" & vbCrLf)
scriptBlock.Append("    {" & vbCrLf)
scriptBlock.Append("        alert($(‘#txtTheOldBox’).attr('value').length);" _
                                               & vbCrLf)
scriptBlock.Append("        _
         alert($(‘#" & txtTheBox.ClientID & "’).attr('value').length);" & vbCrLf)
scriptBlock.Append("    }" & vbCrLf)

We’ve basically copied and pasted our script code here and just added our StringBuilder’s append method to the beginning. I inserted tabs so that the code would be formatted nicely in the rendered code. Also, I added vbCrLf to the end of each line, this puts a Carriage Return/Line Feed at the end of each line. Be careful if you decided to leave these out as this will run all the script onto one line and you need to take care with writing your script to support that, little things can catch it up of you’re not careful.

In our second alert, we’ve taken out hard coding the name of the TextBox and we’ve used the object’s ClientID property to insert this into our rendered code. Now, even if we change what is around our textbox and the .Net code automatically changes the ClientID of the TextBox without our knowing, our code will still target correctly. Run your code and check it out.

Next, let’s tweak our code a little and use one function and pass it a parameter with the TextBox name rather than hard coding names (i.e. let’s make our function reusable). First, let’s modify our countIt function declaration to accept a parameter. Modify it as follows:

scriptBlock.Append("    function countIt(theTextBox)" & vbCrLf)

Comment out the two alert lines in your AddJavascript subroutine and add the following line in their place:

scriptBlock.Append("        alert($('#' + theTextBox).attr('value').length);" _
                          & vbCrLf)

In the front-side code, modify your input button’s onclick= parameter so that it reads:

onclick=“countIt('txtTheOldBox')”

This will call our modified countIt function and pass it it’s name. Now, how do we modify our asp:Button’s onClientClick parameter the same way? Take the onClientClick parameter out of our front-side code entirely, and add the following to your AddJavascript subroutine in the code-behind:

btnNew.OnClientClick = "countIt('" & txtTheBox.ClientID & "')"

Again, we’re making use of the TextBox’s ClientID property so that we don’t have to manually keep track of it on the front side. Run it again, and notice that we now only get one alert. Each button runs the function just on the textbox it is next to.

Epilogue

Combining JavaScript and ASP.Net code can be tricky, especially when attempting to target an ASP.Net control specifically with your script code. Putting the JavaScript in the code-behind makes it easier to accurately target the correct names.

Doing it this way isn’t without it’s pitfalls, it DOES make it harder to write JavaScript (although I tend to write it in the front-side code and then move it to the back once finished). But it’s been a successful strategy for me an I thought I’d pass it on.

The Other Side of ASP.Net Windows Authentication, Part 4

Prologue

ASP.Net Windows authentication seems to be a topic that hasn’t been covered in very much depth. There are plenty of articles about making the change over, however there doesn’t seem to be much information on what to do once you’ve made the transition. This series aims to add some depth to the discussion (Check out previous posts Part 1, Part 2 and Part 3).

Problem

In Part 1, we setup our project and configured it to use Windows authentication. In Part 2 we recreated some functionality that allows us to work with Active Directory Groups as roles. We also created functionality to simulate some of the missing pieces of the Role provider (since much of the Role provider’s functionality is NOT supported under Windows authentication). In Part 3, we created a hybrid Profile object, one that pulls together the SQL Table Provider data and also displays Active Directory Information. In this post, we’ll wrap up with a couple issues that tripped me up and that I couldn’t find much information on.

Solution

A Page Cannot Call Another Page

This particular error was a complete surprise. Our site has several locations where page A may make a call during it’s processing to page B to retrieve some information, I’ll give two examples:

  1. We have a section where Incidents are recorded. Once the incident is saved, an email goes out to those who are signed up for email notification when new incidents are recorded. The save process generates this email. The content of the email is generated by having the entry page, pull a ‘view’ page of the incident and then inserting this page’s html as the email’s content (there are more than 100 fields on the page, rather than recreate the whole page, we just pull the rendered HTML from the view-only page). I’ve outlined how I do this in a previous post.
  2. We have RSS feeds in several places where there are ‘lists’. On the home page, we have a ‘new items’ RSS feed. This feed is compiled by retrieving all the other RSS feeds from the site and then aggregating them into one feed.

In both of these instances, the page goes out and retrieves information from the site by requesting the rendered information from a page. When the page makes to retrieve the other page, I would receive an access denied error. This particular error was a very elusive since it did NOT produce this issue on my local machine during testing and an ASP.Net error (Yellow Screen of Death error) was never generated. Additionally I believed this to be an issue with the RssToolkit control that I was using i.e. it wasn’t passing credentials correctly or something.

I tried a substantial number of methods to pass credentials with the request (I even went so far as to create a new user account in Active Directory that we would use just for this purpose NOTHING worked). Both the locations that I would retrieve the second page from didn’t need to be security protected, so I made sure that I changed the web.config to allow access. Still Access Denied. And I still had no ASP.Net error logs on the server to tell me what was wrong.

I created a quick windows app that would pull the second page, and ran it from several machines (to see if I could retrieve the page the same way as the page was and if the methods of passing credentials would work – they did). I also wanted to make sure that it wasn’t a configuration of the server. It worked fine on every machine except the web server itself. If I took the Windows application to the web server, THEN I would get access denied. It was only an issue ON the web server FROM the web server.

In the end, I found that Windows Server 2008 has a security ‘loopback check’. By default when the server calls back to the server, it will be blocked. An entry will be entered into the security log on the server to the effect of ‘An error occurred during login.’ Microsoft has a KB article regarding this which solved my problem. (Note you can specify which websites on the server can bypass this check. This means that if you server several sites on one host, you can be selective about which ones to allow/bypass the loopback check for).

In the end, the credentials weren’t the problem, it was the loopback protection. And since I did all my development on my XP machine, there was no loopback check and thus, no issue.

Access Denied Errors Don’t Give You Any ASP.Net Error Information

One of the frustrating things about trying to diagnose access denied issues is that they don’t create any ASP.Net errors. For a while now, we’ve been using the ELMAH error logging framework. This has worked well for us and catches errors that the website generates. I get emails regarding errors and can sometime fix errors for our users before they even report them (It’s fun to call a user and say, “that error you’re getting, I fixed it, try it again.”). Access Denied errors however do NOT report to ELMAH or to the event log for that matter. Why?

The crux of the answer is that security is checked BEFORE the ASP.Net engine starts processing anything. So, even though I wasn’t getting an access denied error until after page A requested page B (i.e. ASP.Net processing was in progress) the fact that access was denied was generated BEFORE page B was processed is an immediate show stopper. Since everything stops immediately, an exception is never passed back. What does this mean in practice? Well, there isn’t much you can do about it, but do remember that you can’t troubleshoot any code if you’re getting an Access Denied error because the code never gets processed.

Epilogue

Well, that doesn’t seem like it should have been all that much trouble, but realistically some of these issues held me up for several days of hard and extensive research/troubleshooting. I found the answer to some of them likely by dumb luck. BUT, I did find and answer and hopefully this will help you to avoid the same pit falls.

What other pitfalls have you found with using Windows authentication?

The Other Side of ASP.Net Windows Authentication, Part 3

Prologue

ASP.Net Windows authentication seems to be a topic that hasn’t been covered in very much depth. There are plenty of articles about making the change over, however there doesn’t seem to be much information on what to do once you’ve made the transition. This series aims to add some depth to the discussion (Check out previous posts Part 1 and Part 2).

Problem

In Part 1, we setup our project and configured it to use Windows authentication. In Part 2 we recreated some functionality that allows us to work with Active Directory Groups as roles. We also created functionality to simulate some of the missing pieces of the Role provider (since much of it’s functionality is NOT supported under Windows authentication). In this post we’ll be looking at creating a ‘hybrid’ profile that allows us to use some information from Active Directory as part of our profile (such as Name, Title etc.).

Solution

One of the things that becomes a little muddled when we’re using Active Directory as our authentication method is where do we want to retrieve the information that under Forms authentication would be in our profile. For example, we would normally request First and Last name from the profile, however this information is already in Active Directory. But not all the information that we’d like to record is stored there (for example a signature line). We can still use the built in profile provider with Windows authentication, but now we’d have to use two different objects to pull a complete rather than one. So how do we retrieve ALL the information from one location. We create a hybrid profile.

Setting up the SQLTableProvider

NOTE: I realize that it is probably a bad idea as far as separation of concerns is well, concerned, but this does allow us to put all our profile eggs in one basket so it becomes VERY convenient.

We could probably modify the default profile provider in some means to do what we want, however I’d rather use the SQL Table Provider that Microsoft offers. This gives me quite a bit more leverage with accessing the data and is relatively easy to use. Also, we can easily ‘hybridize’ our profile pretty easily.

Let’s get started. First off, get the the SQL Table Provider. The default method of use for this provider, I believe, is to have you create your classes and then compile them into a DLL that you can drop into your project. I prefer instead to drop in the classes so I can modify them at will in my project. In your App_Code folder, add a ‘Profile’ folder and copy the following three file from your download to this folder:

  • ProfileCommon.vb
  • SqlStoredProcedureProfileProvider.vb
  • SqlTableProfileProvider.vb

(Note: In the SqlStoredProcudureProfileProvider.vb file I found that I needed to add an import for System.Diagnostics).

We’ll need to create a table in our database to handle the profile data. You’ll want to run the RegSQL utility to ready your database for ASP.Net use. This puts in the stored procedures and such that will be used, even if we’re not using the default provider, we may be using pieces of this functionality or stored procedures. In your database, create a new table called CustomProfile. Create your fields as desired, for our purposes, let’s keep it simple and create a Signature field that is nvarchar(255). Also required are two other fields that are used by the provider, LastUpdatedDate as a datetime and UserID as a uniqueidentifier (which is also our primary key).

We need to add a connectionString for the database to our web.config, so make sure that you do that. Then we’ll configure our Profile to use the SQL Table Provider. Remove the existing profile section of your web.config and replace it with the following:

<profile defaultProvider="TableProfileProvider" inherits="Microsoft.Samples.ProfileCommon">
  <providers>
    <clear/>
    <add name="TableProfileProvider" type="Microsoft.Samples.SqlTableProfileProvider" connectionStringName="aspnetDB" table="CustomProfile" applicationName="/"/>
  </providers>
</profile>

Just a couple things to point out. The type is listed as SqlTableProfileProvider, this is one of the classes that we imported for our Profile. Also note that we’ll need to enter the name of the table that we’re using for our CustomProfile. Finally notice that in our profile node, we specify that the profile inherits ‘Microsoft.Samples.ProfileCommon’. This means that we’ll be using this class to specify our profile.

Creating our Hybrid Profile

We need to modify our ProfileCommon so that it will match our database, otherwise we’ll get errors. If you open you ProfileCommon.vb file you should see several examples of how to create properties to match the database fields. We shouldn’t need to create the LastUpdatedDate and UserID, they are built-in (that’s why they’re required). To add the signature field, clear the existing ones (or comment them out) and add the following:

<CustomProviderData("Signature;nvarchar")> _
Public Overridable Property Signature() As String
    Get
        Return DirectCast(Me.GetPropertyValue("Signature"), String)
    End Get
    Set(ByVal value As String)
        Me.SetPropertyValue("Signature", value)
    End Set
End Property

Not to get too specific about it, but note that the CustomeProviderData declaration shows the field name and the database type, this is how the provider will pull the data from the database. Now our provider should be ready to pull the profile information from our database.

Next, let’s add some Active Directory information to our Profile so we can access it via our profile object. In our ProfileCommon.vb file, let’s create a new region just below the second GetUserProfile() method, we’ll add our Active Directory functionality in this region so it is easy to identify.

#Region "Active Directory Information"
        'the functionality in this section does not conform to the other properties
        ‘in this class these properties and the associated functions pull 
        ‘information from Active Directory and return the information as though
        ‘it were part of the profilecommon. The remaining properties
        'outside this section pull their information via the
        ‘SQLTableProfileProvider

#End Region

Next, we’ll create a couple of methods that will pull the user’s information from Active Directory and store it so that we can access it whenever we need to use it. Add the following to your new region (Note, we’ll also need to add an import statement and import the System.DirectoryServices namespace):

'creates and active directory user object for use with the Active directory
Private _objUser As SearchResult = Nothing
Private Function GetUserAccountInfoFromAD() As SearchResult
    'if accessing current user, then me.username will contain the domain
    ‘name. Remove domain (everything before the \ to access just the
    ‘username successfully.

    Return GetUserAccountInfoFromAD(Me.UserName.Split("\").Last())
End Function

Private Function GetUserAccountInfoFromAD(username As String) _
                                                                              As SearchResult
    Dim rootOfAdQuery As String = _ 
            “LDAP://<YourDomain>/DC=<Domain>,DC=<TopLevelDomain>” 
    Dim queryFilterFormat As String = _
                 String.Format("(&(samAccountName={0}) _ 
                 (objectCategory=person)(objectClass=user))", userName)
    Dim result As SearchResult = Nothing 

    Using root As DirectoryEntry = New DirectoryEntry(RootADQuery)
        Using searcher As DirectorySearcher = New DirectorySearcher(root)
            searcher.Filter = queryFilterFormat
            Dim results As SearchResultCollection = searcher.FindAll() 

            result = If(results.Count <> 0, results(0), Nothing)
        End Using
    End Using
    Return result
End Function

These methods will allow us to populate the _objUser object with a current user account from our Active Directory domain. Notice that the first GetUserAccountInfoFromAD method parses the username in the profile object (of which this method is a part) to remove the domain information, since our LDAP query later will be building our domain information before running the AD search.

Our second GetUserAccountInfoFromAD method will accept the username we parsed out and then build an LDAP Query. In our site, I put our information into our config file so that it can be more easily changed if needed. LDAP is a little out of the scope of this article, but if you substitute your domain information in the statement above, I believe it should work:

<YourDomain> = abc.com
<domain> = abc
<TopLevelDomain> = com
For example –> LDAP://abc.com/DC=abc,DC=com .

Next, we’ll create a filter string so that when we query, we’ll pull only the specified user account. Our query specifies the user account name, and that the object type should be a user. We also create an object to receive the results of the query.

To execute the query, we’ll create a new DirectoryEntry object using our LDAP query. We then create a new DirectorySearcher using the DirectoryEntry and attach the query filter to it. Finally we’ll run the search, collect the results and if there is more than one object returned, we’ll just return the first (I don’t remember, but it may be default return an array so we may always need to return the first object).

Now, we’re almost in a position to pull some information from the returned user object and display it as though it was part of the profile object. Before we can call our _objUser object and get properties from it, we want to make sure that if the code retrieving the AD object was called and our _objUser has been or gets populated. We’ll do this by creating a UserAccountInfo property and checking in this property that our _objUser is not Nothing. If it is, we’ll know we need to call our GetUserInfoFromAD() method and populate it. Add the following to your code:

'used to access/and populate if needed the user object from AD
Public Property UserAccountInfo() As SearchResult
    Get
        If IsNothing(_objUser) Then
            _objUser = GetUserAccountInfoFromAD()
        End If
        Return _objUser
    End Get
    Set(ByVal value As SearchResult)
        _objUser = value
    End Set
End Property

Notice that our property will return a SearchResult (our _objUser object) and that it will populate _objUser if it is currently Nothing.

Finally we can add some properties that will return AD values. Add the following properties to our region:

Public ReadOnly Property FirstNameAD() As String
            Get
                Try
                    Return UserAccountInfo.Properties("givenName")(0)
                Catch ex As Exception
                    Return String.Empty
                End Try
            End Get
        End Property 

        Public ReadOnly Property LastNameAD() As String
            Get
                Try
                    Return UserAccountInfo.Properties("sn")(0) 'return surname
                Catch ex As Exception
                    Return String.Empty
                End Try
            End Get
        End Property 

        Public ReadOnly Property EmailAD() As String
            Get
                Try
                    Return UserAccountInfo.Properties("mail")(0)
                Catch ex As Exception
                    Return String.Empty
                End Try
            End Get
        End Property 

        Public ReadOnly Property TitleAD() As String
            Get
                Try
                    Return UserAccountInfo.Properties("title")(0)
                Catch ex As Exception
                    Return String.Empty
                End Try
            End Get
        End Property

Each of the properties will pull our _objUser object via the UserAccountInfo property and then access one of it’s properties. This is done using a string key representing the property to return. The names for the properties don’t necessarily follow a good naming convention, but they can be found by accessing a user object in AD via ‘Active Directory Users and Computers’ on the ‘Attribute Editor’ tab. You’ll also notice that each time we retrieve a property, that it returns an array and we need to select the first element of the array to get what we’re looking for. I’ve also differentiated the property names with AD so that we know that we’re getting FirstNameAD from Active Directory when using it in code. Also note that all the properties are read-only. We’re looking to get information, writing to AD is a whole other challenge that we won’t be covering.

Using our Hybrid Profile Object

Finally, let’s use our profile in the context of our CompanyDirectory page so we can see how it works. Let’s start by adding a GridView to our CompanyDirectory.aspx page, we’ll name it gvDir. We’ll bind our directory to it once we retrieve the information. We could format it and define columns, but we’re more interested in functionality than form.

To add our functionality, go to your CompanyDirectory’s back-end code and let’s add a function to retrieve the directory (i.e. profiles), add the following:

Public Function GetCompanyDirectory() As List(Of ProfileCommon) 

    Dim objUsers As List(Of String) = mySite.Security.GetUsersInRole _
                   ("<AnADGroupWithTheUsersToRetrieve>", _
                    mySite.Security.UserReturnValues.Usernames)
    Dim objDir As New List(Of ProfileCommon) 

    For Each objUser In objUsers
        Dim theProfile As ProfileCommon = _
                     ProfileCommon.GetUserProfile(objUser)
        objDir.Add(theProfile)
    Next 

    Return objDir
End Function

We’ll make use of the GetUsersInRole method that we created in the previous post to pull all the users in a group. We’ll then iterate through that list of users, pull their profiles and add it to a return object. Now we just need to add some code to the page_load event handler to bind to the GridView. Add the following to your page_load event handler:

gvDir.DataSource = GetCompanyDirectory()
gvDir.DataBind()

Now, if you run your CompanyDirectory.aspx page, you’ll see that the GridView displays all the profile information and information from Active Directory, all via one profile object that behaves pretty much like we would expect a profile to behave (except that it doesn’t allow for writing back to AD).

Epilogue

While combining our Active Directory information and our regular profile information into the same class module may not be programming best practices, it sure makes it easier to run everything, now profile includes everything from both stores (the database and Active Directory).

In our next and final post we’ll be looking at some of the little nuance things that caught me up when we made the switch, some things that I didn’t find anywhere else and I spent considerable time fighting, hopefully it will be of help to you as well.

The Other Side of ASP.Net Windows Authentication, Part 2

Prologue

ASP.Net Windows authentication seems to be a topic that hasn’t been covered in very much depth. There is plenty about how to make the official change-over to using it as an authentication method, however there isn’t much about what do do past that point. This series aims to add some depth to the discussion (check out Part 1).

Problem

In Part 1, we setup our project and configured it to use Windows authentication. In Part 2, we’ll be diving into functionality we used to do with Membership and Roles. Having converted an existing site from using Form authentication to Windows authentication, one of the things that became very apparent, very quickly is that the Forms way of using Membership and Roles doesn’t work with Windows authentication. From my experience, I’ve found that we’ll want to throw away all the Membership functionality altogether, and that we can only use SOME of the Role functionality.

An addition, some of the information that we previously gathered in our account creation process that used to be part of the Membership functionality, such as email address, is now available from within the Active Directory objects. We’ll want to use this. (While Profile is also effected in the same way, we’ll discuss this more in the next post).

So, as far as goals for this post, we’ll do the following:

  • Create a class module that we can use to access all of our security related calls. This location will have the functionality needed to poll Active Directory and return information that our Role provider would have done with Forms authentication.
  • Talk briefly about possible Active Directory design to facilitate ‘Role’ functionality.
  • Create some functions that will allow us to retrieve groups and users and some user information from Active Directory.
  • Implement some site security in the Web.config using the <location> elements and our Active Directory roles.

Solution

Setting up Active Directory

We selected to use Active Directory groups to enforce permissions on our Intranet rather than using the build-in Roles provider. This allows us to manage all our security concerns for the network to be managed in one place. We selected to create a new OU for Intranet Permission groups so that they’d be organized in AD.

Since we were converting from Forms authentication, we already had several Roles specified for the site. We created a new group in our Intranet Permissions OU for each Role we had defined in our site and assigned the AD user accounts to each group to coordinate with the Roles they had assigned under Forms authentication. In addition, rather than allowing all Administrators or even Domain Administrators defined in AD to have administrator access to the site, we decided we would rather specify these permissions intentionally and specifically, so we also created an Intranet Administrators group in this OU.

Setting up Site Security

One of the things that I’ve done on our site is to centralize security. This allows me to keep strings that are used for security (such as role/group names) in one central place rather than having them all over the site. In fact, there are only two places in our entire site where role names are entered and would have to be changed if any of the role names were to change; 1. the security class, and 2. the web.config for the <location> security elements (I’ll give a possible 3rd location as our .sitemap files, occasionally/rarely we have to add a role name there so security trimming works correctly).

Let’s start with creating a central security class to handle our security related concerns. Add a new class module to your App_Code folder, name it Security.vb.

We’ll want this to be accessible throughout the site without having to create a concrete instance of our security class so well make it a shared class by adding the ‘NotInheritable’ modifier to the class declaration. Also I’m going to add a namespace: mySite. Our security class should now look about like this:

Namespace mySite
    Public NotInheritable Class Security

    End Class
End Namespace

This class is where all our string literals should be kept and any references to the methods in this class should NOT use string literals, so we’ll be setting up an Enum for the rest of the site to use, and then we’ll be defining a structure to hold all our string literals. This looks like this:

'Enum used to present a security area to the code so actual role names are ‘not used anywhere but here.

Public Enum securityArea
    NotSet
    Administrator
    PressReleaseEditor
    PressReleaseReviewer
End Enum

'The strings that correlate to the roles in the AD to check the user against.
Private Structure AuthorizedRoles
    Public Shared Administrators As String = _
                  "<yourDomain>\Intranet Administrators"
    Public Shared PressReleaseEditors As String = _
                  "<yourDomain>\Press Release Editors"
    Public Shared PressReleaseReviewers As String = _
                  "<yourDomain>\Press Release Reviewers"

    Dim Count As Integer 'A structure must have 1+ NON shared components.
End Structure

A couple of things to point out here:

  • The Enum and the Structure do NOT need to have the same text. Notice that we will simply call administrator using the enum, but we’ll map that to “Intranet Administrators”. Also, Enum elements cannot contain spaces, our AD Groups can.
  • The items in the Enum are singular. We’ll be requesting from the security class to tell us if someone is AN administrator, rather than if they are part of the administrators Group, if they are A reviewer not if they are part of the reviewers.
  • The elements of the structure are the names of the Active Directory groups that we are using to assign permissions to our users. So, in this example, in our domain, we have a group called ‘Intranet Administrators’, and another called ‘Press Release Reviewers’. Since these equate to Active Directory groups, we need to include their Domain information and they CAN include spaces.
  • The structure elements are all shared so that we don’t have to create an instance of the structure to use our strings. Also notice that all structures are required to have at least one element that is NOT shared. I added a count element that I never use to satisfy this requirement.

NOTE: At our company, I’m in a good position to make groups as needed in Active Directory since I am also the domain administrator. If you are not in a position to make groups, you may want to pick some that you know already exist for purposes of testing. We won’t be making any changes to the groups although you are supposed to be able to.

Next, we’ll create a method that returns a a T/F value regarding whether someone is authorized to the roles we created in our Enum. For this, we’ll create a shared function ‘IsUserAuthorized’ and pass in one of the roles we’ve defined in our Enum.

'returns t/f regarding if the current user is authorized in the section specified.
Public Shared Function IsUserAuthorized(ByVal requestedSecuritySection As _
                                                           securityArea) As Boolean
    Dim bIsAuthorized As Boolean = False 

    Select Case requestedSecuritySection
        Case securityArea.Administrator
            bIsAuthorized = Roles.IsUserInRole(AuthorizedRoles.Administrators)
        Case securityArea.PressReleaseEditor
            bIsAuthorized = _
                (Roles.IsUserInRole(AuthorizedRoles.PressReleaseEditors) Or _
                 Roles.IsUserInRole(AuthorizedRoles.PressReleaseReviewers) Or _
                 Roles.IsUserInRole(AuthorizedRoles.Administrators))
        Case securityArea.PressReleaseReviewer
            bIsAuthorized = _
             (Roles.IsUserInRole(AuthorizedRoles.PressReleaseReviewers) Or _
              Roles.IsUserInRole(AuthorizedRoles.Administrators))
        Case Else
            bIsAuthorized = False
    End Select 

    Return bIsAuthorized
End Function

Within the function, we have a Select Case statement that examines the Enum parameter passed in and then calls the Role provider to check on membership in a Group. Since we previously set our Role provider in the web.config to use the WindowsTokenRoleProvider, we CAN actually use the Role class to return whether the user is in a AD group or not without having to code it all (one of the few Role pieces that still seems to work for us).

Notice also, that we are passing into the Role.IsUserInRole function the STRUCTURE’s elements, and not the Enum’s. This method needs a string that equates to the group to look for, i.e. our ‘domain\group name’. On our site, we have several places where there are multiple levels of security (i.e. reviewer and editor), the editor is the lowest security and the reviewer will have all the editor’s permissions as well as additional reviewer’s permissions. So, if someone is a reviewer, they are also an editor by default. This is why we create an OR list of role requests, for example, if someone is a reviewer OR an editor OR an admin, they they are an editor.

If all we wanted to do was check if the user was in a particular group we’d be done, but we may also want to list all the users in a specific group, something along the lines of the Forms authentication’s use of Role.GetUsersInRole() method. Attempting to use this method while configured for Windows authentication will result in an ProviderException. This method cannot be used when using the WindowsTokenRoleProvider. SO, we’ve got to create a work around for it.

We found on our site that actually we wanted to do 2 different things with getting users from groups. 1. we wanted to get the usernames of those in particular groups, and/or 2. we wanted sometimes to get the email addresses of people who were in certain groups. We’ll create functionality for both of these needs.

We’ll create a new Enum that we’ll Name UserReturnValues, allowing us to specify if we want a list of usernames or a list of email addresses to be returned. We’ll define it as follows:

Public Enum UserReturnValues
    Usernames
    Email
End Enum

Next, we’ll create a method that returns a list of strings containing the information requested. We’ll be creating some helper functions in a minute to facilitate the AD query and data extraction. First, let’s create our GetUsersInRole method.

Public Shared Function GetUsersInRole(ByVal authorizedRole As string, _ 
                                         Optional returnValues As UserReturnValues = _ 
                                        UserReturnValues.Usernames) As List(Of String)
    Select Case returnValues
        Case UserReturnValues.Usernames
            Return GetGroupMembers(authorizedRole)
        Case UserReturnValues.Email
            Return GetGroupMembersEmails(authorizedRole)
    End Select
    Return Nothing
End Function

Several things to point out. Note, we will be passing in the string from our AuthorizedRole Structure so that AD can be queried with it. Our second parameter specifies whether to pass back email addresses or usernames. We’ll take the information passed in and call one of two helper functions depending on what we’re looking to return.

Now for the heart of it, the helper functions. First, We’ll need to add some references to System.DirectoryServices and System.DirectoryServices.AccountManagement namespaces. Right-click your solution name in the solution explorer and select Add Reference… Select the .Net tab and add both System.DirectoryServices and System.DirectoryServices.AccountManagement. You’ll also need to import the System.DirectoryServices.AccountManagement namespace into your security class file.

We’ll create a function to retrieve the members of a group. Add the following method to your class:

Private Shared Function GetGroupMembers(sGroupName As String) _
                                                   As List(Of String)
    Dim objReturn As New List(Of String)
    Dim ctx As PrincipalContext = New PrincipalContext(ContextType.Domain)
    Dim group As GroupPrincipal = _
                                    GroupPrincipal.FindByIdentity(ctx, sGroupName) 

    For Each p As Principal In group.Members
        If p.StructuralObjectClass = "group" Then
            objReturn.Add("Group: " & p.Name) ‘this line for demo purposes only
            objReturn.AddRange(GetGroupMembers(p.Name))
        End If
        If p.StructuralObjectClass = "user" Then _
                               objReturn.Add(p.SamAccountName)
    Next 

    Return (From objItem In objReturn Select objItem _
                          Distinct Order By objItem).ToList()
End Function

In our function, we first define a couple of variables: a list object to hold the names we find in our group, a PrincipalContext (the entity we use to search with) and a GroupPrincipal the results of our search against the PrincipalContext, which we’ll immediately populate by calling the FindIdentity method and passing in our context and our group name.

Once we’ve retrieved the members of the group, we’ll cycle through the members of the group and do some work with them. First, we’ll examine what kind of object we’ve got, is it a group, or a user? If it’s a group, we’ll identify it as a group and then add the names in that subgroup by recursively calling the GetGroupMembers method with the group name. (NOTE: I wouldn’t recommend leaving in the line that adds the group name in your production code, however we’ll leave it here for illustration purposes). In our environment, we have several groups that have groups nested within them and they all roll up to make the larger group (i.e. several department groups roll up to make the division group, the division group doesn’t have individuals in it, just groups). If the object is of type user, then we want to add the username (the SamAccountName) to our return object.

Finally, we’ll filter our results some, it is possible that since we’re recursing groups that we’ll end up with the same username several times in our list. This final LINQ statement will go through and make sure that we only return each name one time.

To test this functionality, let’s add a label to our default.aspx and then add the following to the code behind’s Page_Load event handler:

Dim sbUsers As New StringBuilder
For Each sUser In mySite.Security.GetUsersInRole("<a group in your AD>")
    sbUsers.Append(sUser & "<br>")
Next
lblResults.Text = sbUsers.ToString()

Run your application and you should see a listing of the users in it. Also, any sub groups will be listed (under ‘G’ because we alphabetized the list in our LINQ statment).

Next, lets implement the GetGroupMembersEmails method. This is much the same as the GetGroupMembers with the exception that we have to convert our Principal object to a UserPrincipal object to retrieve the email address property. Add the following to your Security class:

Private Shared Function GetGroupMembersEmails(sGroupName As String) As List(Of String)
    Dim objReturn As New List(Of String)
    Dim ctx As PrincipalContext = New PrincipalContext(ContextType.Domain)
    Dim group As GroupPrincipal = _
                GroupPrincipal.FindByIdentity(ctx, sGroupName)

    For Each p As Principal In group.Members
        If p.StructuralObjectClass = "group" Then
            objReturn.Add("Group: " & p.Name) ’this line for demo purposes only
            objReturn.AddRange(GetGroupMembersEmails(p.Name))
        End If
        If p.StructuralObjectClass = "user" Then
            Dim up As UserPrincipal = CType(p, UserPrincipal)
            objReturn.Add(up.EmailAddress)
        End If
    Next 

    Return (From objItem In objReturn Select objItem Distinct _
                   Order By objItem).ToList()
End Function

Notice that it’s basically the same as the GetGroupMembers method except the if/then statement where we handle if the principal’s StructuralObjectClass is user. In that instance, we create a new UserPrincpal object and then cast our Principal object to it. Once we do that, we can pull the EmailAddress property rather than the SamAccountName.

We can test this functionality by changing the statement in our code-behind for the default.aspx page, We just need to add the optional parameter to our call to GetUsersInRole to specify which UserReturnValues to use, and set it to Email. so your complete line might look like this:

For Each sUser In mySite.Security.GetUsersInRole("<a group in your AD>",  _
          mySite.Security.UserReturnValues.Email)

Run your application and you’ll see that we pulled all the email addresses for the users in the group. In addition, you’ll see in the ‘G’ section that it recursed any groups within this group. (I suggest taking that line out of the method though so that you don’t end up with errors from group names in the return values).

As a side note, this may not be an example of good compartmentalization and separation of concerns in our application. The Security class may not be the proper location for the GetGroupMemberEmails function, but I would argue that it is for our GetGroupMembers function since we’re using it in the context of permissions on our site. Either way, you may decide that either one or both actually belongs in another module all together, which I can’t argue against.

Adding Site Security in Web.Config

As a final section of this post, we’ll add a protected page to our site and then use <location> to enforce security based on Active Directory groups. This is pretty easy to implement, the only real difference between Forms and Windows authentication is that we’ll need to include domain information in the Role name. We’ll do one just for the save of having done it.

Add a new folder to your site and name it ‘Admin’. Inside that folder, add a new .aspx page named CompanyDirectory.aspx. We don’t need to do anything with this page, we’ll just be proving security with it at this point (we’ll add functionality to it in a later post).

Open your Web.config, and add a security section for the page just before your system.web section as follows:

<location path="Admin">
  <system.web>
    <authorization>
      <allow roles="<Your Domain>\Intranet Administrators"/>
      <deny users="*"/>
    </authorization>
  </system.web>
</location>

If you want to test whether this is working, you can try visiting the CompanyDirectory page with a group that you ARE included in, and then try it again with a group you are NOT included in and see if security is working properly. When you select a group you are NOT in, you’ll get an access denied error. (As a quick note since we’ll cover it in a later post, when you get this access denied error, you’ll find that it happens BEFORE the page is processed and so you don’t get any of the error reporting might expect. Also, you may not get any kind of notification of the access denied in the event viewer – which makes it harder to troubleshoot).

Epilogue

In this post, we took a look at some of the functionality that we can still get from our Roles provider and some of the functionality that we can’t. We created a means of retrieving the members of a Role (an AD Group) and for retrieving their email addresses. Finally, we took a quick look at using the AD groups for site security in our web.config file.

In the next post, we’ll look at creating a ‘hybrid’ profile, a cross between using our profile provider and retrieving user information from Active Directory.

The Other Side of ASP.Net Windows Authentication, Part 1

Prologue

We recently converted our Intranet from Forms authentication to Windows authentication. While this has several distinct advantages for us (i.e. single-sign on, users don’t have to manage another account etc.), it has not been without some significant pitfalls as well.

In searching the internet for advice on how to make this transition, I found plenty of information about how to configure your web.config to use Windows authentication, but I found a distinct lack of anything helping to make a full transition from Forms to Windows authentication. Once you get it converted, there wasn’t much to help you past that point.

Some of the challenges I found with Windows authentication include:

  • Membership and Roles do NOT function the same now that you’re using Windows authentication. Profile does mostly, however existing profiles do NOT automatically come across (not a big deal to us).
  • Development on your machine using the Cassini engine does NOT handle permissions the same as running in IIS on Windows 2008. Cassini is more lenient.
  • Now you can’t debug as a different user, every time you run your site, you’re automatically logged in as YOU.
  • Errors that trigger an ‘access denied’ response, are not logged as errors. Since authentication happens before the page is processed, there isn’t anywhere to look at/capture access denied errors, since they aren’t ASP.Net errors. (In addition, it wasn’t even generating errors in the event viewer, more on that in a later post).

Problem

Very quickly I found that the traditional means of pulling information for the user no longer works as it used to before convertion. For example, with Forms authentication, you can get information from the user account such as email using memberhip.getuser(). This is not the case when using Windows authentication. Also, now I now want to pull some information that USED to be in the profile (such as first and last name) from Active Directory rather than the profile.

What I intend to do with this series is to share several things that we did so that we could retrieve account/role/profile information more easily, that had became a real bear once we implemented Windows authentication.

This will be a multi-part series and specifically I’ll be covering a number of points:

  • Configuring your site to use Windows authentication (getting your site setup) – this post..
  • Using “Roles” with windows authentication to specify permissions. I’ll be using Active Directory groups rather than the built in Roles manager to assign permissions. This way we can keep administration of permissions all within Active Directory.
  • Retrieving “profile” information from Active Directory. Rather than using the default built-in profile provider, I also converted to the SQLTableProfileProvider form Microsoft. This makes it much easier to pull profile information for multiple users at one time and I can now user Entity Framework whereas with the built-in provider, I cannot.
  • Creating a ‘hybrid” profile for the user, pulling some information from AD and some from the database using the SQLTableProfileProvider.
  • Using some of the pieces above to create a Company Directory page, thus demonstrating how we use this hybrid profile.
  • Some other snags that you may run into with Windows authentication and how to work around them.

With no further ado, Let’s get started.

Solution

NOTE: To take advantage of this series, you’ll have to access to an Active Directory domain. Some of the things we’ll do in the series may require you to have access to create AD users/OUs/groups etc, if you don’t have rights, do what you can.

Start by creating a new website.

Let’s make the changes we need to the Web.Config so that we can run in Windows authentication. In the <system.web> section, add an authentication section:

<authentication mode=”Windows” >
</authentication>

You may want to remove <forms loginUrl=”” /> subnode, however I don’t believe that it is strictly necessary.

To gain SOME Role functionality (in my experimentation, this DOESN’T give us full use of the Role functionality, only partial functionality, we’ll also modify our Roles section as follows:

<roleManager defaultProvider="AspNetWindowsTokenRoleProvider"
           enabled="true" cacheRolesInCookie="true" cookieName="myRoles">
  <providers>
    <clear/>
    <add name="AspNetWindowsTokenRoleProvider"
               type="System.Web.Security.WindowsTokenRoleProvider"
               applicationName="/" />
  </providers>
</roleManager>

You’ll notice that we removed the default SqlRoleProvder from the provider list since we won’t be using it and made the WindowsTokenRoleProvider our default provider.

For the moment, that’s it for the we.confg. We’ll make some changes to the Profile information in a later post when we implement the SqlTableProvider to do a custom profile.

Also, if you are converting an existing site, and already have authorization information in your web.config (<location> sections), we’ll have to make some modifications to the permissions to those as well, however we’ll also address this in a later post.

If you used, the ‘Asp.Net Web Site’ template rather than the ‘Asp.Net Empty Web Site’ template, your site.master page already contains some login controls that will demonstrate that you are successfully logged in using your AD credentials. If you used the empty site, you can add the following to your default page:

<div class="loginDisplay">
    <asp:LoginView ID="HeadLoginView" runat="server"
              EnableViewState="false">
        <AnonymousTemplate>
            [ <a href="~/Account/Login.aspx" ID="lsStatus" runat="server">Log In</a> ]
        </AnonymousTemplate>
        <LoggedInTemplate>
            Welcome <span class="bold">
                             <asp:LoginName ID="lnName" runat="server" />
                          </span>!
            [ <asp:LoginStatus ID="lsStatus" runat="server"
                      LogoutAction="Redirect" LogoutText="Log Out"
                      LogoutPageUrl="~/"/> ]
        </LoggedInTemplate>
    </asp:LoginView>
</div>

Run your application and you should see your domain and username placed the LoginName control and that you have a Logout link rather than a Login link showing that you are currently logged in.

Epilogue

This gets us all setup to start using our site with Active Directory. In the next installment, we’ll start using Active Directory groups to provide Role security and we’ll also look at accessing Active Directory groups programmatically to see if someone has adequate permissions.

Using a Custom Profile UserControl in a CreateUserWizard Control

Prologue

One of the features I’ve been working on lately is adding membership/profiles to our website. In the past we haven’t offered this feature on our site, but recently we’ve made some changes that make this a good idea. In the process of trying to do this, I found that I would like the user to be able to enter/edit profile information on two different pages, the registration page and a page to edit account information (profile page). If I’m editing the same information in more than one location, this just begs to be put into a custom control. The question becomes, how do we integrate this custom control, with a CreateUserWizard control on the registration page?

Problem

Creating a profile control isn’t really all that difficult, but there are a couple of things that can make a profile control a little tricky to integrate with the CreateUserWizard. Some sticky spots I found included:

  • Some fields of the control should be hidden depending on if we are adding or edit a user (i.e. change password should NOT be shown when adding a user).
  • If the user is created, and then profile creation doesn’t validate, the profile never gets created. If I can validate BOTH user account and profile at the same time then we can prevent one from succeeding and the other failing.
  • Setting validation groups for your profile control so they match the CreateUserWizard control and give feedback at the same time as the CreateUserWizard.
  • Getting your profile control’s validation errors to display in your CreateUserWizard’s ValidationSummary (ok, the CreateUserWizard doesn’t have one, but we’ll create one).

We’ll talk about a these in this post and try to get the basics for creating a pretty usable profile control.

Solution

To begin, we’ll need to do a little setup. Obviously, you’ll need a project with membership and profiles set up. If you need help setting up a membership DB, you can review the information in the solution section here. Make sure you have an account to login with defined, and then also make sure that you deny unauthenticated users access to your EditProfile.aspx page.

We’ll also need to define a profile in our web.config, we’ll keep it pretty simple, and add the following to the profile section in your web.config:

<properties>
  <add name="FirstName" type="String" />
  <add name="LastName" type="String" />
  <add name="Phone" type="String" />
  <add name="AvatarUrl" type="String" />
  <add name=”Signature” type=”String” />
</properties>

Basically, we’re only going to do a couple fields so we can get a feel for doing validation different ways in our control, and so that we can also look at doing file uploads (the Avatar picture).

Let’s create a UserControl to house our profile control, let’s name it ProfileControl.ascx. Let’s also create two pages which we can use to test this control, a registration page (register.aspx) and a profile page where the user can edit their profile (EditProfile.aspx). We’ll want to reference the profile control on both pages so drag the control onto the page so it creates the <%@ Register directive. Then you can remove the markup for the control, we’ll add it back in later.

Let’s start by putting together the ProfileControl. The look I wanted was to have the Avatar image on the left, and all the fields on the right (see image). The fields displayed should include some of the information from the CreateUserWizard when the user is on the EditProfile page, but NOT when on the Registration page i.e. username, email). Also, the image will not appear on the registration page. Finally, we want to allow the user to change their password, email and username ONLY if it is allowed and configured on the site (i.e. we want to be able to enable/disable this ability), and they are on the EditProfile page. To get this layout, we’ll create a table.

If you look at the image, showing our layout, you may think right off the bat that we should make a 4 column table, however we want the image to be displayed next to SEVERAL rows rather than limited to it’s own row. So what I did is create the first row with two Cells, one that has the image and one for the title. The image cel has a rowspan that will include ALL the rows of the table, and another that has a colspan of at least 2 (so it will be a title and not blow out our field labels cell). The next row will have 3 cells. This effectively gives us a 4 column table (the image cell + these three cells, since the image cell goes all the way down). - (As a note, if you look at the above image, you’ll see fields that we aren’t using. I didn’t include them in the code since it makes this article too lengthy, but you can see how the control could be expanded to include more information if desired).

We’ll continue to make the table, when it is finished, it’s markup should look about like this:

<asp:Table ID="tblProfile" runat="server">
    <asp:TableRow>
        <asp:TableCell RowSpan="25" VerticalAlign="Top">
            <asp:Image id="imgAvatar" runat="server" Height="100"
                        Width="100" BorderWidth="1" />
        </asp:TableCell>
        <asp:TableCell ColumnSpan="2">
            <asp:Label ID="lblPersonalInfo" Text="Personal Information"
                  runat="server" Font-Bold="true" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow ID="trUsername" runat="server" Visible="false">
        <asp:TableCell HorizontalAlign="Right">
            <asp:Label ID="lblUsernameLabel" runat="server"
                   Text="Username:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:Label ID="lblUsername" runat="server" Text="" />
        </asp:TableCell>
        <asp:TableCell HorizontalAlign="Right">
            <asp:LinkButton ID="lnkbtnChangeUsername" runat="server"
                       Text="Change" />
        </asp:TableCell>
    </asp:TableRow> 
    <asp:TableRow ID="trEmail" runat="server" Visible="false">
        <asp:TableCell HorizontalAlign="Right">
            <asp:Label ID="lblEmailLabel" runat="server" Text="Email:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:Label ID="lblEmail" runat="server" Text="" />
        </asp:TableCell>
        <asp:TableCell HorizontalAlign="Right">
            <asp:LinkButton ID="lnkbtnChangeEmail" runat="server"
                 Text="Change" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow ID="trPassword" runat="server" Visible="false">
        <asp:TableCell HorizontalAlign="Right">
            <asp:Label ID="lblPasswordLabel" runat="server"
                       Text="Password:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:Label ID="lblPassword" runat="server" Text="********" />
        </asp:TableCell>
        <asp:TableCell HorizontalAlign="Right">
            <asp:LinkButton ID="lnkbtnChangePassword" runat="server"
                Text="Change" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow>
        <asp:TableCell Width="130px" HorizontalAlign="Right">
           <asp:Label ID="lblFirstName" runat="server"
               Text="First Name:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:TextBox ID="txtFirstName" runat="server"
                   MaxLength="255" />
            <asp:RequiredFieldValidator ID="rfvRequiredFirstName"
                 runat="server" Display="Static" Text="*"
                 ControlToValidate="txtFirstName"
                 ErrorMessage="First name is required." />
            <ajaxToolkit:TextBoxWatermarkExtender ID="tweTxtFirstName"
                 runat="server" WatermarkText="First Name"
                 TargetControlID="txtFirstName" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow>
        <asp:TableCell HorizontalAlign="Right">
           <asp:Label ID="lblLastName" runat="server"
                Text="Last Name:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:TextBox ID="txtLastName" runat="server" MaxLength="255" />
            <asp:RequiredFieldValidator ID="rfvRequiredLastName"
                  runat="server" Display="Static" Text="*"
                  ControlToValidate="txtLastName"
                  ErrorMessage="Last name is required." />
            <ajaxToolkit:TextBoxWatermarkExtender ID="tweTxtLastName"
                runat="server" WatermarkText="Last Name"
                TargetControlID="txtLastName" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow>
        <asp:TableCell HorizontalAlign="Right">
           <asp:Label ID="lblAddress" runat="server"
                 Text="Address:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:TextBox ID="txtAddress" runat="server" Width="100%"
                    MaxLength="255" />
            <ajaxToolkit:TextBoxWatermarkExtender ID="tweTxtAddress"
                 runat="server" WatermarkText="Enter Address"
                TargetControlID="txtAddress" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow>
        <asp:TableCell HorizontalAlign="Right">
           <asp:Label ID="lblHomePhone" runat="server"
                  Text="Home Phone:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:TextBox ID="txtHomePhone" runat="server"
                   MaxLength="12" />
            <asp:RegularExpressionValidator ID="revHomePhone"
                runat="server" Display="Dynamic"
                ControlToValidate="txtHomePhone"
                Text="*"
           ErrorMessage="Only valid phone numbers allowed for Home Phone."
               ValidationExpression="^(1?(-?\d{3})-?)?(\d{3})(-?\d{4})$" />
            <ajaxToolkit:TextBoxWatermarkExtender ID="tweTxtHomePhone"
                runat="server" WatermarkText="xxx-xxx-xxxx"
                TargetControlID="txtHomePhone" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow>
        <asp:TableCell HorizontalAlign="Right">
           <asp:Label ID="lblAvatar" runat="server"
                Text="Profile Picture:" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:FileUpload ID="fuAvatar" runat="server" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:LinkButton ID="lnkbtnClearPict" runat="server" Text="Clear" />
        </asp:TableCell>
    </asp:TableRow>
    <asp:TableRow>
        <asp:TableCell HorizontalAlign="Right"><asp:Label ID="lblSignature"
              runat="server" Text="Signature" />
        </asp:TableCell>
        <asp:TableCell>
            <asp:TextBox ID="txtSignature" runat="server"
                   TextMode="MultiLine" Width="250px" Rows="3"/>
            <ajaxToolkit:TextBoxWatermarkExtender ID="tweTxtSignature"
                  runat="server" WatermarkText="Insert your signature"
                  TargetControlID="txtSignature" />
        </asp:TableCell>
    </asp:TableRow>
</asp:Table>

A couple of things to note. I’m a big fan of the TextBoxWaterMarkExtender in the Ajax control toolkit and so I use them here. In addition, all the pieces that we may want to disable, such as changing username/email have links to change them rather than having a textbox(es) to immediately change them (i.e. new password textboxes). Also notice that the TableRow associated to these fields has an ID defined and Visible is set to false, this allows us to hide/reveal these fields. All required fields have validators added however note that NONE of them have their ValidationGroup property set, we’ll handle that later so they can match the ones assigned the the CreateUserWizard control. You may also notice that the control contains no button for saving the information. We’ll handle that from the page that uses our ProfileControl, and provide a method in our control for saving and loading. Finally, the AvatarURL field has a clear button. This allows the user do delete their AvatarUrl and go back to the default undefined one (otherwise, they’d have to assign another image to replace the existing one).

Speaking of saving, let’s create our routines for saving and loading of information to the profile control. To do this, we’ll setup a pair of public methods that can be called from the page containing the control. First however, we’ll add a couple of helper functions for handling the avatar image. Add the following helper functions to your control’s code-behind:

'takes a data stream (from a FileUploadControl for instance), and saves it as a jpg with a maximum dimension also passed in.
    'ratio is preserved, so the max dimension could be either horizontal or vertical.
    Public Sub ResizeAndSavePicture(ByVal stream As System.IO.Stream, _
                   ByVal sNewFileName As String, ByVal maxDimension As Integer)
        'retrieve original picture
        Dim objSourceBitmap As New Bitmap(stream) 

        'Size pic to max dimension and preserve the ratio
        Dim dblRatio As Double = 0.0
        If objSourceBitmap.Width > objSourceBitmap.Height Then
            dblRatio = GeneratePictureRatio(maxDimension,  _
                  objSourceBitmap.Width)
        Else
            dblRatio = GeneratePictureRatio(maxDimension, _
                 objSourceBitmap.Height)
        End If
        Dim imgWidth As Integer = objSourceBitmap.Width * dblRatio
        Dim imgHeight As Integer = objSourceBitmap.Height * dblRatio 

        'create a copy of original at new size then save the copy
        Dim objTargetBitmap As New Bitmap(imgWidth, imgHeight)
        Dim objGraphics As Graphics = Graphics.FromImage(objTargetBitmap)
        Dim objEncoder As EncoderParameters
        Dim recCompression As New Rectangle(0, 0, imgWidth, imgHeight)
        objGraphics.DrawImage(objSourceBitmap, recCompression)
        objEncoder = New EncoderParameters(1)
        objEncoder.Param(0) = New EncoderParameter(Encoder.Quality, 90)
        objTargetBitmap.Save(sNewFileName, GetEncoderInfo("image/jpeg"), _
                  objEncoder) 

        'cleanup
        objGraphics.Dispose()
        objTargetBitmap.Dispose()
    End Sub 

    'returns the ratio of desired size to current size.
    Public Shared Function GeneratePictureRatio( _
              ByVal DesiredSize As Integer, _
             ByVal CurrentSize As Integer) As Double

        Return DesiredSize / CurrentSize
    End Function 

    'retrieves the ImageCodecInfo for a correspoinding Mime string
    Public Shared Function GetEncoderInfo( _
                     ByVal sMime As String) As ImageCodecInfo

        Dim objEncoders As ImageCodecInfo()
        objEncoders = ImageCodecInfo.GetImageEncoders()
        For iLoop As Integer = 0 To objEncoders.Length - 1
            If objEncoders(iLoop).MimeType = sMime Then
                Return objEncoders(iLoop)
            End If
        Next
        Return Nothing
    End Function

Credit to the internet in general, cause I’m sure I pulled most of that from the internet. Without going into too much detail, basically this allows us to save our image, resized to the appropriate dimensions while preserving proportions.

Next, let’s add the functionality to save our profile information. Add the following to the code-behind for the ProfileControl:

Public Sub SaveProfile(ByVal sUsername As String)
    Dim theProfile As ProfileCommon = Profile.GetProfile(sUsername)
    If IsNothing(theProfile) Then theProfile = _
                  ProfileCommon.Create(sUsername, True) 

    theProfile.FirstName = txtFirstName.Text
    theProfile.LastName = txtLastName.Text
    theProfile.Address = txtAddress.Text
    theProfile.Phone = txtHomePhone.Text
    theProfile.Signature = txtSignature.Text 

    If fuAvatar.HasFile() Then
        theProfile.AvatarUrl = sUsername & ".jpg"
        ResizeAndSavePicture(fuAvatar.PostedFile.InputStream, _
              Server.MapPath("~/Images/" & sUsername & ".jpg"), 100)
        imgAvatar.ImageUrl = "~/Images/" & theProfile.AvatarUrl
    End If
    theProfile.Save()
End Sub

The code isn’t really all that complex, we retrieve the user’s profile from the DB, and if retrieval returns nothing, then that means we need to create a profile since it doesn’t exist. Once we have the profile, we populate it from our form. The interesting part is saving the images. If the FileUpload control has a file, then we save the AvatarUrl into the profile (I’m changing the filename to be the username), resize the image, save it to our images folder and then set our imgAvatar.ImageUrl to be this location. Finally, we call the profile’s save function and it is persisted to the database.

Next, let’s add the LoadProfile subroutine. Add the following to your control’s code-behind:

Public Sub LoadProfile(ByVal sUsername As String)
    Dim theProfile As ProfileCommon = Profile.GetProfile(sUsername)
    If Not IsNothing(theProfile) Then
        txtFirstName.Text = theProfile.FirstName
        txtLastName.Text = theProfile.LastName
        txtAddress.Text = theProfile.Address
        txtHomePhone.Text = theProfile.Phone
        txtSignature.Text = theProfile.Signature 

        Dim muPerson As MembershipUser = Membership.GetUser(sUsername)
        If Not IsNothing(muPerson) Then
            lblUsername.Text = muPerson.UserName
            lblEmail.Text = muPerson.Email
        End If 

        If theProfile.AvatarUrl.Length > 0 Then
            imgAvatar.ImageUrl = "~/Images/" & theProfile.AvatarUrl
        Else
            imgAvatar.ImageUrl = "~/Images/" & NoAvatarSetPicture
        End If
        imgAvatar.Visible = True
    End If
End Sub

Again, not too complex, we’ll pull the user’s profile from the DB and populate our control’s textboxes. We also pull the user’s MembershipUser object so we can get the email. If an avatarUrl is set, we’ll set the imgAvatar.ImageUrl from the profile, and if not, then we’ll set it to the default blank avatar picture (Note: it’s using a property we’ll define here in a second). Finally, we make sure that the image is visible.

Since we want to be able to turn on or off the change password ect. links, we’ll set up a number of properties for our control so we can configure them when we use the control. Add the following properties to the top of your control’s code-behind:

Public Property NoAvatarSetPicture() As String = ""
Public Property AllowChangePassword() As Boolean = False
Public Property AllowChangeEmail() As Boolean = False
Public Property AllowChangeUsername() As Boolean = False
Public Property AllowClearAvatarPicture() As Boolean = False
Private _ShowAvatar As Boolean = True
Public Property ShowAvatar As Boolean
    Get
        Return _ShowAvatar
    End Get
    Set(ByVal value As Boolean)
        _ShowAvatar = value
        imgAvatar.Visible = _ShowAvatar
    End Set
End Property
Public Enum userControlModes
    EditUser
    AddUser
End Enum
Public Property Mode() As userControlModes = userControlModes.AddUser

You’ll notice that with the ShowAvatar property, we also set the imgAvatar to be visible. Also, the Mode property has an enumeration defined for it. Other than that they are just straight up properties.

We also need to do something with all these properties when we load the page. For that we’ll create a helper function to set visibility. It takes into account the mode as well as settings in the properties. Add the following to your controls’ code-behind:

Private Sub SetVisibility()
    Select Case Mode
        Case userControlModes.EditUser
            lnkbtnChangePassword.Visible = AllowChangePassword
            lnkbtnChangeUsername.Visible = AllowChangeUsername
            lnkbtnChangeEmail.Visible = AllowChangeEmail
            lnkbtnClearPict.Visible = AllowClearAvatarPicture
            trPassword.Visible = True
            trUsername.Visible = True
            trEmail.Visible = True
        Case Else
            trPassword.Visible = False
            trUsername.Visible = False
            trEmail.Visible = False
            lnkbtnClearPict.Visible = AllowClearAvatarPicture
    End Select
End Sub

If we are editing the profile, then we’ll set everything accordingly. If the profile is NOT being edited (i.e. added) then we don’t want to display the password, email and username options since they haven’t yet been created. We need to call SetVisibility from our Page_Load event handler, so add the following to your Page_Load event hander:

If Not Page.IsPostBack Then
    SetVisibility()
End If

Now, if you test your page, you should be able to disable/enable pieces by changing the properties for the control in the page’s markup.

We also want to be able to set our control so that validation in our control happens at the same time as the CreateUserWizard. To do this, we’ll expose a property that allows us to set the ValidationGroup for our profile control and it will set the ValidationGroup of all the validation controls within our profile control. Add the following to your code-behind:

Private _ValidationGroup As String = ""
Public Property ValidationGroup() As String
    Get
        Return _ValidationGroup
    End Get
    Set(value As String)
        _ValidationGroup = value
        SetValidationGroups()
    End Set
End Property

Private Sub SetValidationGroups()
    rfvRequiredFirstName.ValidationGroup = _ValidationGroup
    rfvRequiredLastName.ValidationGroup = _ValidationGroup
End Sub

We’ve created a property so that we can assign a ValidationGroup to control as a whole just like any other control, the one little difference is that we’re calling the SetValidationGroups subroutine in our code. This will go through and assign this ValidationGroup to all the validation controls in our control.

Before we go onto using the control on a page, we also want to add functionality to change the email address, password and username (if we allow it). For this, I used the ModalPopupExtender in conjunction with the LinkButtons next to the information. This allows us to NOT have to put in multiple textboxes for things such as old/new/confirm passwords and unnecessarily crowd our page. We’ll create some Modal dialogs on our markup page that we can utilize. Add the following to your HTML code:

<asp:UpdatePanel ID="upnlPopUps" runat="server"> 
   <ContentTemplate>
      <%--Change password modal dialog--%>
      <asp:Panel ID="pnlChangePassword" runat="server"
            SkinID="ModalPopup" style="display: none;">
         <asp:ChangePassword ID="cpChangePassword" runat="server" />
      </asp:Panel>
      <asp:Button ID="btnFakeMpeChangePassword" runat="server"
               style="display: none;" />
      <ajaxToolkit:ModalPopupExtender ID="mpeChangePassword"
            runat="server"  PopupControlID="pnlChangePassword" 
            TargetControlID="btnFakeMpeChangePassword" />

      <%--Change username modal dialog--%>
      <asp:Panel ID="pnlChangeUsername" runat="server"
               SkinID="ModalPopup" style="display: none;">
         <asp:Table ID="tblChangeUsername" runat="server">
            <asp:TableRow>
               <asp:TableCell ColumnSpan="2" HorizontalAlign="Center">
                  <asp:Label ID="lblChangeUsername" runat="server"
                     Text="Change Username" Font-Bold="true" />
               </asp:TableCell> 
            </asp:TableRow>
            <asp:TableRow>
               <asp:TableCell SkinID="FieldLabel">
                  <asp:Label ID="lblOldUsernameLabel" runat="server"
                        Text="Current Username:" />
                  </asp:TableCell> 
                  <asp:TableCell>
                     <asp:label ID="lblOldUsername" runat="server" Text="" />
                  </asp:TableCell>
               </asp:TableRow> 
               <asp:TableRow> 
                  <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblNewUsernameLabel" runat="server"
                        Text="New Username:" />
                  </asp:TableCell> 
                  <asp:TableCell> 
                     <asp:TextBox ID="txtNewUsername" runat="server" />* 
                  </asp:TableCell>
               </asp:TableRow> 
               <asp:TableRow> 
                  <asp:TableCell HorizontalAlign="Center" ColumnSpan="2"> 
                     <asp:RequiredFieldValidator ID="rfvRequiredNewUsername"
                           runat="server" ControlToValidate="txtNewUsername"
                           Text="New Username is required."
                           ErrorMessage="New Username is required."
                           Display="Dynamic" ValidationGroup="NewUsername"
                           CssClass="validationPasswordRecovery" /> 
                  </asp:TableCell>
               </asp:TableRow> 
               <asp:TableRow> 
                  <asp:TableCell ColumnSpan="2" HorizontalAlign="Right" > 
                     <asp:LinkButton ID="lnkbtnSaveNewUsername"
                           runat="server" Text="Save"
                           ValidationGroup="NewUsername" />
                     &nbsp;&nbsp;&nbsp; 
                     <asp:LinkButton ID="lnkbtnCancelNewUsername"
                           runat="server" Text="Cancel" /> 
                  </asp:TableCell>
               </asp:TableRow>
            </asp:Table>
         </asp:Panel>
        <asp:Button ID="btnFakeMpeChangeUsername" runat="server"
            style="display: none;" />
        <ajaxToolkit:ModalPopupExtender ID="mpeChangeUsername"
               runat="server" PopupControlID="pnlChangeUsername" 
               TargetControlID="btnFakeMpeChangeUsername" /> 
       
      <%--Change email modal dialog--%>
      <asp:Panel ID="pnlChangeEmail" runat="server" SkinID="ModalPopup"
               style="display: none;">
         <asp:Table ID="tblChangeEmail" runat="server">
            <asp:TableRow>
               <asp:TableCell ColumnSpan="2" HorizontalAlign="Center">
                  <asp:Label ID="lblChangeEmail" runat="server"
                        Text="Change Email" Font-Bold="true" />
                  </asp:TableCell>
            </asp:TableRow> 
            <asp:TableRow> 
               <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblCurrentEmailLable" runat="server"
                              Text="Current Email:" />
               </asp:TableCell> 
              <asp:TableCell>
                  <asp:label ID="lblCurrentEmail" runat="server"
                           Text="" />
              </asp:TableCell> 
            </asp:TableRow> 
            <asp:TableRow>
               <asp:TableCell SkinID="FieldLabel">
                  <asp:Label ID="lblNewEmail" runat="server"
                     Text="New Email:" />
               </asp:TableCell> 
               <asp:TableCell> 
                  <asp:TextBox ID="txtNewEmail" runat="server" />* 
               </asp:TableCell> 
            </asp:TableRow> 
            <asp:TableRow> 
               <asp:TableCell HorizontalAlign="Center" ColumnSpan="2"> 
                  <asp:RequiredFieldValidator ID="rfvRequiredNewEmail"
                        runat="server" ControlToValidate="txtNewEmail"
                        Text="New Email is required."
                        ErrorMessage="New Email is required." Display="Dynamic"
                        ValidationGroup="NewEmail"
                        CssClass="validationPasswordRecovery" /> 
                   <asp:RegularExpressionValidator ID="revEmailAddress"
                         runat="server" Text="Email address is not formatted
                        correctly. Please enter as: abc@xyz.com"
                        ErrorMessage="Email address is not formatted correctly.
                        Please enter as: abc@xyz.com" Display="Dynamic"
                        ControlToValidate="txtNewEmail"
                        ValidationExpression="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" 
                        ValidationGroup="NewEmail"
                        CssClass="validationPasswordRecovery" />
               </asp:TableCell>
            </asp:TableRow> 
            <asp:TableRow> 
               <asp:TableCell ColumnSpan="2" HorizontalAlign="Right" >
                  <asp:LinkButton ID="lnkbtnSaveNewEmail" runat="server"
                     Text="Save" ValidationGroup="NewEmail" />
                  &nbsp;&nbsp;&nbsp; 
                  <asp:LinkButton ID="lnkbtnCancelNewEmail" runat="server"
                           Text="Cancel" />
               </asp:TableCell>
            </asp:TableRow>
         </asp:Table>
      </asp:Panel>
      <asp:Button ID="btnFakeMpeChangeEmail" runat="server"
               style="display: none;" />
      <ajaxToolkit:ModalPopupExtender ID="mpeChangeEmail" runat="server"
            PopupControlID="pnlChangeEmail"
            TargetControlID="btnFakeMpeChangeEmail" /> 

      <%—Feedback message modal dialog--%>
      <asp:Panel ID="pnlMessage" runat="server" SkinID="ModalPopup"
                     style="display: none; text-align: center;">
         <asp:Label ID="lblMessage" runat="server" /><br /><br />
         <asp:LinkButton ID="lnkbtnCloseMessage" runat="server"
               CausesValidation="false" Text="Close" />
      </asp:Panel> 
      <asp:Button ID="btnFakeMpeMessage" runat="server"
                style="display: none;" />
      <ajaxToolkit:ModalPopupExtender ID="mpeMessage" runat="server"
            PopupControlID="pnlMessage"
            TargetControlID="btnFakeMpeMessage" />

    </ContentTemplate>
</asp:UpdatePanel>

Ok, that seems like a lot, but what we really did was create an update panel (so we can do a partial page post back with our modal dialogs) and put in 4 modal pop up dialogs. One each for changing username, password and email as well as a feedback message dialog. Each has ModalPopup it’s own ValidationGroup, and each has its own ValidationSummary (different than the profile control’s, and already preset here).

Next, we’ll hook the LinkButtons up so that when they are clicked, they’ll call these Popups. We’ll actually do this via javascript, but we’ll wire it up in our code-behind. Add the following to your Page_Load event handler, right before the SetVisibility() function call:

lnkbtnChangeEmail.OnClientClick = _
         "$find('" & mpeChangeEmail.ClientID & "').show(); return false;"
lnkbtnChangePassword.OnClientClick = _
        "$find('" & mpeChangePassword.ClientID & "').show(); return false;"
lnkbtnChangeUsername.OnClientClick = _
        "$find('" & mpeChangeUsername.ClientID & "').show(); return false;"
lnkbtnCancelNewUsername.OnClientClick = _
        txtNewUsername.ClientID & ".value='';"
lnkbtnCancelNewEmail.OnClientClick = txtNewEmail.ClientID & ".value='';"

Here, we’re setting the OnClientClick property of each of our links (and a couple from our dialogs) so that we can run this via javascript rather than requiring a post back. Notice that we’re inserting the ClientID property so that we get the generated ControlID.

Finally, we’ll create event handlers so that we can handle when the user clicks ‘Save’ in our dialogs. We’ll need to do something special with changing the username since there isn’t any built in functionality (in the membership stuff). Also, the email address may need to be changed in more than just the membership tables, so we’ll want to be able to know when it is changed and possibly make other changes. SO, we’ll create events in our control and pass this information up to the page utilizing the profile control so it can make these changes. To do this, we’ll add two event declarations to the general declarations section of our control’s code-behind:

Public Event ChangeUsername(ByVal sender As Object, _
             ByVal e As upcEventArgs)
Public Event ChangeEmail(ByVal sender As Object, ByVal e As upcEventArgs)

You’ll notice that we have custom EventArgs being passed out as parameters, we’ll need to define those in our code-behind also. Add the following to your code-behind:

Public Class upcEventArgs
        Inherits EventArgs 

        Public Sub New() 
        End Sub 

        Public Sub New(ByVal vOldValue As String, ByVal vNewValue As String)
            OldValue = vOldValue
            NewValue = vNewValue
        End Sub 

        Public Property OldValue As String
        Public Property NewValue As String
End Class

This is pretty generic, just a way of passing both the old and new values out to the event.

Now, we can handle our save events by passing the information up to the page utilizing the control. Add the following to your lnkbtnSaveNewEmail Click event handler:

RaiseEvent ChangeEmail(Me, New upcEventArgs(lblCurrentEmail.Text,  _
     txtNewEmail.Text))

and the following to your lnkbtnSaveNewUserName Click event handler:

RaiseEvent ChangeUsername(Me, New upcEventArgs(lblOldUsername.Text, _
      txtNewUsername.Text))

We dont’ have to handle changing the password because the ChangePassword control actually handles the changing for us, however we do want to present some information to the user incase this fails, or it succeeds. To do this, we’ll add handlers for events on the ChangePassword control.

We’ll send a message on success using the message ModalPopup, add the following to the ChangePassword’s ChangedPassword event handler:

lblMessage.Text = "Password successfully changed."
mpeMessage.Show()

and we’ll add the following to the ChangePasswordError event handler:

lblMessage.Text = "Password change failed. Check that you entered your password correctly and that your <br> new password meets the length requirements." 
mpeMessage.Show()

Now, we can concentrate on using our control to in our pages. Let’s start with the EditProfile.aspx page. We don’t need much on this page to make it work, we just need the control and then a button to trigger saving our profile control. Add the following to your EditProfile.aspx page within your form tags (make sure you register the control in your markup declarations):

<uc1:ProfileControl ID="upcProfile" runat="server" 
     ValidationGroup="WholePage" Mode=”EditUser” /> 
<asp:LinkButton ID="lnkbtnSaveProfile" runat="server" Text="Save Profile" 
     ValidationGroup="WholePage" />&nbsp;&nbsp; 
<asp:LinkButton ID="lnkbtnResetProfile" runat="server"
     Text="Reset Profile" />

We’ve created two LinkButtons, one to save the profile and one to reset it. The save button has a ValidationGroup set as does our profile control, so saving will trigger validation in all our profile fields.

Let’s handle the information in the code-behind. To reset our profile, we just need to reload the page. To do that, add the following to the lnkbtnRestProfile’s Click event handler:

Response.Redirect(Request.Url.ToString())

We just send the page back to itself and it’ll reload.

To handle saving the profile, we need to call the control’s SaveProfile method in the lnkbtnSaveProfile’s click event handler:

upcProfile.SaveProfile(HttpContext.Current.User.Identity.Name)

We call the SaveProfile method and pass in the currently logged in user. The nice thing is that we can add additional pieces outside the profile to our page and then handle them in this same event handler (we allow users to sign up for notifications for instance. This information we put into a different table in the database and this is handled right here AFTER we call SaveProfile).

We’ll also need to load our profile (so users can see what their profile currently holds). We can do this on in our controls’ load event handler. Add the following:

upcProfile.LoadProfile(HttpContext.Current.User.Identity.Name)

Really, this is all we NEED to do for the EditProfile.aspx page. You may want to go back to the markup and add some attributes for attributes like the blank AvatarURL and play with enabling some of the links for changing password etc.

Now let’s concentrate on the CreateUserWizard and it’s interaction with our Profile control. We’ll start work on the Register.aspx page. We could just add a CreateUserWizard control to our page and then put our profile control right under it, however this would have the two controls working independently. We’d rather put them together so they work as one. To do this, we’ll have to insert our profile control INTO our CreateUserWizard control. We could create the template by hand if we wanted, but we can also select the CreateUserWizard control in design view and use the smart tag to ‘Customize Create User Step’. This will create a table with the elements of the CreateUserStep, everything is in place, we just need to add  our user profile control. To do this, create a new row in the table after your last entry control and move our ProfileControl into it as such:

<tr>
    <td colspan="2">
        <uc1:ProfileControl ID="upcProfile" runat="server"
                 AllowChangeEmail="true" Mode="AddUser" ShowAvatar="false"
                 ValidationGroup="CreateUserWizard1" />
    </td>
</tr>

You’ll notice that our ValidationGroup attribute was already set on all the validation controls, we simply need to match our profile control’s ValidationGroup attribute to the other controls and it will now be part of the validation group. Also, we’re setting the mode to ‘AddUser’, this will hide the controls in our profile control that deal with changing password etc.

We might also want to add a validation summary at the end of our CreateUserWizardSte and capture all the error messages. We’ll actually do some extra stuff with it too so we can do more custom validation with our control (so we can report things such as duplicate email addresses or invalid entries). You’ll notice that the CreateUserWizard uses a Literal control for error messages, and this is where these errors are passed back. If we want them to be in our ValidationSummary, we have to do it a little differently, and we’ll grab those in our code-behind. So replace the literal control with the following:

<asp:PlaceHolder ID="plCustomStuff" runat="server" />
<asp:ValidationSummary ID="vsSummary" runat="server"
           ValidationGroup="CreateUserWizard1" DisplayMode="BulletList" />

If you change the validation controls by adding display=”Dynamic”, the ValidationSummary control is currently set to correctly display all errors, including our profile control’s errors.

Now we’re ready for some code behind. Creation of the user account and all the pieces of that are handled by the CreateUserControl so we just need to figure out when the account has been created and then save the profile. To do this, we need to add the following to our CreateUserWizard’s CreatedUser event handler:

Dim upcTheProfileControl As ProfileControlWithCUW_ProfileControl

upcTheProfileControl = _
CreateUserWizardStep1.ContentTemplateContainer.FindControl("upcProfile")

Dim username As TextBox = _
CreateUserWizardStep1.ContentTemplateContainer.FindControl("username")

upcTheProfileControl.SaveProfile(username.Text)

There are several things to note here. First, we’re creating a variable to hold our profile control, the type is defined by our class name in the control’s code behind. Next, we pull our profile control from the page so we have the actual instance being used in the page. To do this, we need to do a find control, not on the CreateUserWizard, but on the actual STEP that were in. My CreateUserWizardStep is named CreateUserWizardStep1 and we use that to retrieve both the profile control and the username textbox. Then we call the profile control’s SaveProfile method, passing it the username.

What happens if the user selected a duplicate username or some other issue? How do we handle that? We’ll add the following to the CreateUserWizard’s CreateUserError event handler:

Dim plStuff As PlaceHolder = _
          cuwsCreateUser.ContentTemplateContainer.FindControl("plCustomStuff")
Dim cvCreateError As New CustomValidator()
Dim sErrorMessage As String = ""
cvCreateError.IsValid = False
cvCreateError.ValidationGroup = "cuwCreateUser"

Select Case e.CreateUserError
    Case MembershipCreateStatus.DuplicateEmail
        sErrorMessage = cuwCreateUser.DuplicateEmailErrorMessage
    Case MembershipCreateStatus.DuplicateUserName
        sErrorMessage = cuwCreateUser.DuplicateUserNameErrorMessage
    Case MembershipCreateStatus.InvalidAnswer
        sErrorMessage = cuwCreateUser.InvalidAnswerErrorMessage
    Case MembershipCreateStatus.InvalidEmail
        sErrorMessage = cuwCreateUser.InvalidEmailErrorMessage
    Case MembershipCreateStatus.InvalidPassword
        sErrorMessage = String.Format(cuwCreateUser.InvalidPasswordErrorMessage,
                  Membership.Provider.MinRequiredPasswordLength, _
                  Membership.Provider.MinRequiredNonAlphanumericCharacters)
    Case MembershipCreateStatus.InvalidQuestion
        sErrorMessage = cuwCreateUser.InvalidQuestionErrorMessage
    Case MembershipCreateStatus.InvalidUserName
        sErrorMessage = "The site administration has determined that username is invalid."
    Case MembershipCreateStatus.UserRejected
        sErrorMessage = "The site administration has rejected that username."
End Select

cvCreateError.ErrorMessage = sErrorMessage
cvCreateError.Visible = False
plStuff.Controls.Add(cvCreateError)

Here, we’re using the same technique as we did to retrieve the profile control to get our PlaceHolder control, then we’ll create a CustomValidtor control, we’ll set our Validation Group, set our IsValid property to false (i.e. NOT valid). We’ll determine the nature of the error, and set the CustomValidator’s ErrorMessage with an appropriate message. Finally, we hide the control, and then add it to our PlaceHolder control. By doing this, we’ll see these errors in our validation summary control. Because the user was never created, we don’t have to worry about prematurely saving the profile (saving profile is in the event handler for AFTER the account is created).

From this point, we can then treat our CreateUserWizard just as we would normally.

Epilogue

There it is, a very clean way of multi-purposing our profile control, and integrating it right into our CreateUserWizard so that it is part of an integrated solution. If the user doesn’t fill in enough of the profile, they can’t create the account, certain profile information is required before account creation can happen.

Retrieving HTML From Another Page For an Email

Prologue

We’ve been working recently on an intranet site for our company. One of the sections we created with the intention of being able to email people when a new item was added. This is easy enough to do, but I wanted to email the details of the new item. Problem was that the contents of the page could be pretty extensive, what I’d really like to do is take the details page for the item and email THAT to the list (thus I wouldn’t have to duplicate code).

Problem

What I’d like to do is retrieve the guts of an Item.aspx page when we submit a new item using AddItem.aspx. Once the item is added, we’ll pull it’s details from the Item.aspx page and use that to email. In theory, for purposes of this post, we’ll simply retrieve contents of another page and pretend to email with it, but close enough. (I got much of my start from the 2nd answer here).

Solution

First, let’s get things setup. We’ll need a page to retrieve HTML from. Create a page for this called Item.aspx. For our purposes, we’ll add a label control, and then in the page load event handler for the page assign some text there. I’m doing this so we can show that the page will be processed (i.e. we can retrieve data from the db and everything) via this method.

Next, we’ll need a page to use to do the retrieval of the HTML. Add an page called AddItem.aspx. We’ll add 2 controls to this page, a linkbutton called lnkbtnRetrieveIt and a label called lblRetrieved. We’re now ready to go.

So let’s create a function that we can call that will retrieve the HTML of our Item.aspx page. We’ll need to import the System.Net namespace to get it to work, and then we’ll add a function to our AddItem code-behind as follows:

Public Function GetHtmlOfUrl(ByVal url As String) As String
    Dim myClient As New WebClient()
    Dim requestHTML As Byte()
    Dim utf8 As UTF8Encoding = New UTF8Encoding 

    requestHTML = myClient.DownloadData(url)
    Return utf8.GetString(requestHTML)
End Function

Then, let’s add some code to the lnkbtnRetrieveIt’s click event handler. When we click the link, we want to retrieve the text, and then set lblRetrieved’s text with that text. Add the following to the lnkbtnRetrieveIt’s click event handler:

Dim sFullText As String = GetHtmlOfUrl("~/item.aspx")
lblRetrieved.Text = sFullText

I’m using a variable to store it rather than just putting it to the text of the label so that we can do some manipulations with it shortly. If you run your application and click the link, you’ll find that you are unsuccessful in retrieving the page. This is because the url isn’t resolved to remove the ~/ in the url. You can either code the entire url yourself (http://…. ect) or you can do some other finagling. If you’re pulling from another site, you might want to type the whole thing out, but since I’m pulling from another page in my site, I’d rather not. We’ll finagle.

We’ll add two helper functions. I actually created these in my basepage that I used on our site that way I have them always at hand, so you’ll see them here as properties. One returns the fullbase of the path (i.e. http://xyz… and in the case of VS.Net development, that includes the port number and the virtual directory…) and the other is called by it so it’s here. Add the following to your AddItem code behind page:

'returns full absolute path to root directory http:xyz.com/xyz/
Public ReadOnly Property FullBaseUrl() As String
    Get
        Return Me.Request.Url.AbsoluteUri.Replace _
               (Me.Request.Url.PathAndQuery, "") & Me.BaseUrl
    End Get
End Property

'returns absolute path to the root directory /xyx/
Public ReadOnly Property BaseUrl() As String
    Get
        Dim url As String = Me.Request.ApplicationPath
        If url.EndsWith("/") Then
            Return url
        Else
            Return url & "/"
        End If
    End Get
End Property

Now we just need to modify the call to GetHtmlOfUrl in our lnkbtnRetrieveIt’s click event handler so it reads:

Dim sFullText As String = GetHtmlOfUrl(FullBaseUrl & "item.aspx")

Now it should work.

But what if we want SOME of the text of the other page? If you look at the source of your page after you click the link, you’ll notice that we’re inserting the ENTIRE page’s content into our label’s text property. We may not want to do that. So what to do?

You’ve got several options, you can regex the stuff you want out, that can get complicated really quick, I actually wanted to pull the item details of of the page and so I handled it by putting in some code that I could reference after I pulled the text, and then got a substring. To do this, go back to your Item.aspx page and add this line BEFORE our label control:

<!-- getTextBody -->

and this one after it:

<!—end getTextBody -->

Because they are comments, they are hidden when rendered, but can be used by our page as markers for where to begin and end our retrieval. Then we can call substring in our method to pare down what we assign to our label control in our AddItem.aspx page. Modify your assignment to lblRetrieve so it looks like this:

lblRetrieved.Text = sFullText.Substring( _
   sFullText.IndexOf("<!-- getTextBody –>") + _
   "<!-- getTextBody -->".Length, _
   (sFullText.IndexOf("<!-- end getTextBody –>") _
    - sFullText.IndexOf("<!—getTextBody –>")) _
    - "<!-- getTextBody -->".Length)

It looks a little daunting, but basically what we’re doing is getting the text between the two tags (substring would find the beginning of the tag, so we have to account for the tag length and take it out of both ends). Now if you run it, you’ll see that we’ve only retrieved the contents of the label control and not the full page.

Epilogue

For us, this was a great way for us to reuse code from another page. To create an email and generate all this HTML by some other method was going to be a tremendous pain or at the very least, a lot of redundancy, this works very well and suits our needs very well.

More Tweaks for Your ASP.Net Menu Control

Prologue

Recently, I was putting together a new intranet site for our company. I ran into a couple of things that I wanted to do with our Menu that couldn’t be done out of the box with the ASP.Net Menu control. Thanks to the internet (a tweak to the code in the answer here) and a little playing around, I was able to successfully tweak the ASP.Net Menu control to do what I needed.

One little note of warning, your mileage may vary depending on how you have your menu’s sitemap setup. You’ll want to pay attention the notes section in the first tweak.

Problem

Two menu problems that I ran into (kind of related), and was successful in fixing:

1. When using a SiteMapPath control that is bound to the same .sitemap file as the menu, I wanted to have ALL the pages on the site show as part of the breadcrumb, however, I DON’T want them all to show up in the menu.

2. I wanted a Login and Logout option on the menu and I wanted them to be displayed based on the current login status of the user. If the user was logged in, then I wanted it to say Logout, and if not logged in, I wanted it to say Login.

Solution

Let’s first start with some setup, then we’ll get into coding. First, we’ll need a page to test with, name it MenuTest.aspx. In this page, we’ll want to add a SiteMapPath control (named smpTweak), a Menu control (mnuTweak) and a SiteMapDataSource (smdsTweak). Configure both the SiteMapPath and the SiteMapDataSource’s SiteMapProvider to be ‘menuTweak’. Also make sure that for our example you set ShowStartingNode to TRUE (you’ll see why a little later). Set the Menu control’s DataSourceID to smdsTweak.

Next, add the following to your web.config in the System.Web section

<siteMap enabled="true">
  <providers>
    <add name="menuTweak" type="System.Web.XmlSiteMapProvider" siteMapFile="MenuTweaks.sitemap" securityTrimmingEnabled="true" />
  </providers>
</siteMap>

This will setup the data source for all our navigation controls. Notice that the siteMapFile setting is set to the name of the sitemap file you’ll create in just a second.

Finally, create a sitemap file that can be used as our data source. Add a sitemap file named MenuTweaks.sitemap. For contents, add the following:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="~/default.aspx" title="Home"  description="">
        <siteMapNode url="~/menutest.aspx" title="MenuTweaks"  description="" />
        <siteMapNode url="~/default.aspx?2" title="subPage2"  description="" />
    </siteMapNode>
</siteMap>

Now we’re ready to get started.

Hiding Menu Items

On our intranet site, we have two pages that are related, let’s call them Items.aspx and Item.aspx. The Items.aspx page contains a GridView showing all the Items. The Item.aspx page will be called with a querystring identifying the specific item to display. Items.aspx will link to Item.aspx?id=x. I wanted to have Item.aspx show up in the breadcrumb, but I don’t want it to be on the menu, because a querystring will be required when navigating to it. If I leave the page out of the sitemap file entirely, then the SiteMapPath just defaults to blank, so NO breadcrumb will be shown, undesirable as well. So what to do? If only there was a way to specify that a siteMapNode could be visible=false… Let’s do it!

One of the interesting features of the siteMapNode, is that when we process it, any custom attributes we add to it will be ignored by default (i.e. it won’t cause anything to crash). This comes in handy sometimes, like when you want to specify a target, or if we want to add a visible attribute. In our example, we’ll pretend that our menuTest.aspx page requires a querystring, so we don’t want to show it on the menu, but we DO want to have it be part of the breadcrumb.

To do this, add an attribute to the siteMapNode for the MenuTest.aspx page as follows:

visible=”false”

Because this attribute has no meaning to the control itself, we’ll have to handle it’s usage ourselves. To do this, we’ll go to the code-behind and add some code to the mnuTweak’s MenuItemDataBound event handler. When the node is created, we’ll take a look at it and see if its visible attribute is set to false. If it is, we’ll then remove it so it doesn’t show. Add the following code to your event handler:

Dim sTheNode As SiteMapNode = CType(e.Item.DataItem, SiteMapNode)
If Not String.IsNullOrEmpty(sTheNode("visible")) Then
    If sTheNode("visible").ToLower() = "false".ToLower() Then
        e.Item.Parent.ChildItems.Remove(e.Item)
    End If
End If

First, we retrieve the node, then check IF it has a visible attribute, and if it does, then we check it’s value. If the value is false, then we’ll remove the node from the parent’s childItems collection.

Now if you run the application, and navigate to your MenuTest.aspx page, you should see a full breadcrumb but NOT see the page in the menu control.

A couple quick warnings notes on this solution:

  • In our example here, if we choose to hide the starting node in our SiteMapDataSource, we’ll get an exception because the parent node doesn’t exist (or in other words, it IsNothing). If we don’t show the starting node, it won’t be created at all, and so our 2nd level nodes will actually be top level nodes with NO PARENT. Attempting to remove the node will throw an “object reference not set” exception since you’re trying to modify a parent object that is Nothing. In my site, this wasn’t an issue since all the hidden items were 3rd level nodes so when I hid the starting node, they (3rd level) still had parent nodes (2nd level). If anyone has a better workaround that doesn’t have this issue, I’m all ears.
  • Also the reason the item still appears in the breadcrumb is that we aren’t removing the node when it is bound to the SiteMapPath control (in case you were wondering).

Showing Menu Items Based on Current Login Status

In my site, I wanted to add a link that would direct the user to either login or logout depending on their current login status. I attempted to do this at first through security trimming, however you can’t specify any allow/deny settings in the SiteMapNode to do this. The solution is much like hiding a menu item, with only minor differences.

To start with, we’ll add two new SiteMapNode lines to the sitemap file. One for login, and one for logout as follows:

<siteMapNode url="~/default.aspx?3" title="Login" />
<siteMapNode url="~/default.aspx?4" title="Logout" />

Since we don’t have a login page in our site, I’m just pointing them to the default page, but in my intranet site, these both point to login.aspx. The logout had an additional querystring: logout=true that is read on page load and handled if necessary to automatically log the user out.

Now we just need to examine the nodes as they are databound, and see if the user is authenticated, if so, remove the login node, if not, remove the logout node. Add the following to your MenuItemDataBound event handler below the code we created earlier.

'removes the login menu if the user is authenticated
If e.Item.Text.ToLower() = "login" Then
    If HttpContext.Current.User.Identity.IsAuthenticated Then _
                 e.Item.Parent.ChildItems.Remove(e.Item)
End If
'removes the logout menu if the user is NOT authenticated.
If e.Item.Text.ToLower() = "logout" Then
    If Not HttpContext.Current.User.Identity.IsAuthenticated Then _
                    e.Item.Parent.ChildItems.Remove(e.Item)
End If

We’re simply checking the node’s text and taking appropriate action to remove the one that doesn’t apply. NOTE: the same warning applies here as in the previous section. If the node doesn’t have a parent, you can’t remove it (in our intranet, these are off a main menu option called ‘My Account’).

And if you’re interested in the code for automatically logging the user out, I simply added the following to the page’s Load event handler on the target page (login.aspx on our site) for the logout menu:

If Not Request.QueryString("logout") = Nothing Then
    FormsAuthentication.SignOut()
    Session.Abandon()
    Response.Redirect("~/login.aspx")
End If

As I said, my querystring was logout=true, so we check to see if logout is present, and if so, we sign the user out, clear their setting out and refresh the page.

Epilogue

For us, this has worked quite well. As long as there is a node representing the page in the sitemap file, it will show up on the breadcrumb. As we wanted. I also found that it since we’re not worried about what order the visible=false items are shown in (since they’re not shown on the menu), I can just add these items at the bottom of the node group, so they’re all together, something akin to this:

<siteMapNode url="~/Admin/Links.aspx?1" title="Site Admin"> 
  <siteMapNode url="~/Admin/MemberAdmin.aspx" title="Admin Users" />
  <siteMapNode url="~/Admin/errorLog.aspx" title="Error Logs" />
  <!-- Items after this point are labeled visible=false and are here only for breadcrumb purposes -->
  <siteMapNode url="~/Admin/Depts.aspx" title="Admin Depts"
      visible="false" />
  <siteMapNode url="~/Admin/Lists.aspx" title="Admin Lists"
      visible="false" />
</siteMapNode>

Notice that I have a commented line, and then all the visible=false entries.

Like I said, it works well for us.

Displaying CreateUserWizard’s Provider Errors in a ValidationSummary

Prologue

The CreateUserWizard control is a great way to do user creation without having to do much coding. It will validate automatically against your provider’s settings for things like password strength and complexity etc. It’s also nice in that you can customize it using a template and add other profile information into the mix. I did find however, that getting some of the provider level errors to propagate up to my ValidationSummary control was a little tricky.

Problem

When you customize the CreateUserWizard control, you can make a template in a CreateUserWizardStep element. When you do this, you add your textbox controls for username and password etc, and then add validation to them. I added a ValidationSummary to the page, and then when the user goes to create the account, the ValidationSummary will show any areas where the validation fails (such as blank required fields or invalid email addresses).

I found however, that provider level errors (like the password isn’t as long as required in the provider declaration in the Web.config) aren’t processed until you attempt to submit to the database. Because of this, these errors aren’t added to the ValidationSummary, and as such, the user doesn’t get feedback through that mechanism about provider level issues (there is a different mechanism for showing provider errors, IF you create it as part of your template). I didn’t want a second mechanism I wanted all errors to display in the ValidationSummary. I had a difficult time finding anything, but I pieced together several posts (such as this) and came up with a solution.

Solution

First off, we need to create a registration page. Mine contained extra profile information, but for our purposes, we just need the minimum to make this work. The following is the markup of my page:

<asp:CreateUserWizard ID="cuwCreateUser" runat="server"
                                    LoginCreatedUser="true"
                                    CreateUserButtonText="Create My Account">
   <WizardSteps>
      <asp:CreateUserWizardStep ID="cuwsCreateUser" runat="server" >
         <ContentTemplate>
            <asp:Table ID="tblRegister" runat="server" >
               <asp:TableRow>
                  <asp:TableCell ColumnSpan="2">
                     <asp:Label style="position: relative; left: 130px;"
                           ID="lblAccountInfo" runat="server"
                           Text="Account Information"
                           SkinID="FieldLabels" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell SkinID="FieldLabel" Width="200px">
                     <asp:Label runat="server" ID="lblUsername"
                              Text="Username:" />
                  </asp:TableCell>
                  <asp:TableCell>
                     <asp:TextBox ID="Username" runat="server" />
                     <asp:RequiredFieldValidator ID="UserNameRequired"
                           runat="server" ControlToValidate="Username"
                           ErrorMessage="Username is required."
                           ValidationGroup="cuwCreateUser" Text="*" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblPassword" runat="server"
                           Text="Password:" />
                  </asp:TableCell>
                  <asp:TableCell>
                     <asp:TextBox ID="Password" runat="server"
                           TextMode="Password" />
                     <asp:RequiredFieldValidator ID="PasswordRequired"
                              runat="server" ControlToValidate="Password"
                              ErrorMessage="Password is required."
                              ValidationGroup="cuwCreateUser" Text="*" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblConfirmPassword" runat="server"
                           Text="Confirm Password:" />
                  </asp:TableCell>
                  <asp:TableCell>
                     <asp:TextBox ID="ConfirmPassword" runat="server"
                              TextMode="Password"/>
                     <asp:RequiredFieldValidator ID="ConfirmPasswordRequired"
                              runat="server" ControlToValidate="ConfirmPassword"
                              ErrorMessage="Confirm password is required."
                              ValidationGroup="cuwCreateUser" Text="*" />
                     <asp:CompareValidator ID="PasswordCompare" 
                              Runat="server" ControlToValidate="ConfirmPassword"
                              ControlToCompare="Password" Display="Dynamic"
                              ErrorMessage="The Password and confirm password
                              must match." Text="*"
                              ValidationGroup="cuwCreateUser" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblEmail" runat="server" Text="Email:" />
                  </asp:TableCell>
                  <asp:TableCell>
                     <asp:TextBox ID="Email" runat="server" />
                     <asp:RequiredFieldValidator ID="EmailRequired"
                           runat="server" ControlToValidate="Email"
                           ErrorMessage="Email is required."
                           ValidationGroup="cuwCreateUser" Text="*" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblQuestion" runat="server"
                           Text="Security Question:" />
                  </asp:TableCell>
                  <asp:TableCell>
                     <asp:TextBox ID="Question" runat="server" />
                     <asp:RequiredFieldValidator ID="QuestionRequired"
                              runat="server" ControlToValidate="Question"
                              ErrorMessage="Security question is required."
                              ValidationGroup="cuwCreateUser" Text="*" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell SkinID="FieldLabel">
                     <asp:Label ID="lblAnswer" runat="server"
                           Text="Security Answer:" />
                  </asp:TableCell>
                  <asp:TableCell>
                     <asp:TextBox ID="Answer" runat="server" />
                     <asp:RequiredFieldValidator ID="AnswerRequired"
                           runat="server" ControlToValidate="Answer"
                           ErrorMessage="Security answer is required."
                           ValidationGroup="cuwCreateUser" Text="*" />
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell ColumnSpan="2"><br />
                     <asp:ValidationSummary ID="vsSummary" runat="server"
                              ValidationGroup="cuwCreateUser"
                              DisplayMode="BulletList"
                              HeaderText="Correct the following:" />
                  </asp:TableCell>
               </asp:TableRow>
            </asp:Table>
         </ContentTemplate>
      </asp:CreateUserWizardStep>
      <asp:CompleteWizardStep ID="cuwsFinished" runat="server" >
         <ContentTemplate>
            Thank you for setting up your new account.<br /><br />
            <asp:Label ID="lblResults" runat="server" />
         </ContentTemplate>
      </asp:CompleteWizardStep>
   </WizardSteps>
</asp:CreateUserWizard>

In this markup, we create a CreateUserWizard that has been customized. We have all the necessary fields, and validation controls. They all have the same validation group so that they’ll all show up in the ValidationSummary. If you leave the passwords blank for instance, you’ll get a notice in the ValidationSummary that they cannot be blank. BUT, if you fill everything out, and leave the passwords to short, you’ll get the page to postback, a user won’t be created, and you’ll get no message. We will fix that.

Basically, the control is expecting to spit it’s error content out to a literal(?) control with a specific name. I didn’t create that (and have no intention of doing so). Additionally, that wouldn’t put it in the validation control. To do that we need to handle the CreateUserWizard’s CreateUserError event. This will give us the appropriate error, then we’ll need to get it into our ValidationSummary. To do this, we’ll examine the EventArgs and do a select case statement determining the message to send back to the user. Add the following to your CreateUserWizard’s CreateUserError event handler:

Dim sMessage As String = ""
   Select Case e.CreateUserError
      Case MembershipCreateStatus.DuplicateEmail
         sMessage = cuwCreateUser.DuplicateEmailErrorMessage
      Case MembershipCreateStatus.DuplicateUserName
         sMessage = cuwCreateUser.DuplicateEmailErrorMessage
      Case MembershipCreateStatus.DuplicateUserName
         sMessage = cuwCreateUser.DuplicateUserNameErrorMessage
      Case MembershipCreateStatus.InvalidAnswer
         sMessage = cuwCreateUser.InvalidAnswerErrorMessage
      Case MembershipCreateStatus.InvalidEmail
         sMessage = cuwCreateUser.InvalidEmailErrorMessage
      Case MembershipCreateStatus.InvalidPassword
         sMessage = _
               String.Format(cuwCreateUser.InvalidPasswordErrorMessage, _   
               Membership.Provider.MinRequiredPasswordLength, _
               Membership.Provider.MinRequiredNonAlphanumericCharacters)
      Case MembershipCreateStatus.InvalidQuestion
         sMessage = cuwCreateUser.InvalidQuestionErrorMessage
      Case MembershipCreateStatus.InvalidUserName
         sMessage = "Username is not valid"
      Case MembershipCreateStatus.ProviderError
         sMessage = "Something went wrong in the provider"
      Case MembershipCreateStatus.UserRejected
         sMessage = "Administrator said NO WAY!"
   End Select

Notice that we don’t have to even come up with our own messages most of the time, they are already preconfigured in the code (although we can customize them if so desired). You’ll also notice that with the invalid password message, we need to add the String.Format function around it. The default message has 2 variables to be replaced in it so we need to do this, or we’ll end up with {0} and {1} in our code (try it and see).

Now we need to insert the message into our ValidationSummary. To do this, what we’ll do is create a CustomValidator, assign the message and mark it as invalid. Then we just add it to our page. To do this, jump back to the markup, and add a PlaceHolder control just before the ValidationSummary control as follows:

<asp:PlaceHolder ID="phCustomStuff" runat="server" />

This will give us a container to add our CustomValidator control to. Next hop back to our code behind, and we’ll need to retrieve the PlaceHolder control from inside the CreateUserWizard. Then we’ll create and populate our CustomValidator and add it to the page. Add the following to after your Select Case statement:

Dim phStuff As PlaceHolder = _
     cuwsCreateUser.ContentTemplateContainer.FindControl("phCustomStuff")
Dim cvError As New CustomValidator()
cvError.ValidationGroup = "cuwCreateUser"
cvError.ErrorMessage = sMessage
cvError.IsValid = False
phStuff.Controls.Add(cvError)

You’ll notice that we have to extract our PlaceHolder from the ContentTemplateContainer, but once we do that, we just set our CustomValidator and then add it to our PlaceHolder and viola’. Try it. It works.

Epilogue

It seems like it would have been intuitive for the CreateUserWizard to have a ValidationGroup property that we could just set and let it propagate its own errors to. But alas, it doesn’t and we have to do the work ourselves, thankfully the work isn’t that hard, and the error messages are built in, although I found it rather surprising that I never found a full answer to this specific topic anywhere in my searches on the web.

A Better ASP.Net Member/Role Management Page Pt. 10

Prologue

Ok, one more stab at getting it all in. My last post covered issues with chapter 4 in regards to Membership and Roles in the newest version of ASP.Net Problem-Design-Solution. After reading that, I had several other ideas that hadn’t occurred to me previously. I’ll be filling in several “gaps”. Really for the most part, all the stuff I’ll be doing in this post are optional ‘niceities’.

If you aren’t familiar with this series here’s the rundown. If you want to follow along with the other posts, you can create the management page yourself by reviewing all the posts for part 1, part 2, part 3, part 4, part 5, part 6, part 7, part 8 and part 9. Welcome to part 10 (some day I hope to finish).

Problem

I found in working through chapter 4, that I didn’t consider several pieces of a management control that would be nice to have, things like information regarding the user account: last login, created date, is the user online etc. In addition, I didn’t have any means of unlocking a user account if it was locked (by invalid password attempts). We’ll be adding several little things like that in this post, specifically we’ll add:

  • An identifier column for whether the user is online or not. I chose to do a simple red/green for offline/online. We’ll allow CssClass to be passed in or each of the online and offline styles.
  • A column with a checkbox for unlocking the user account – but only if it is currently locked. This will operate like our active checkbox, where it posts back and makes the change immediately.
  • A column with an info link so that we can get information regarding the use’s account such as created date and last login. This will display a modal popup (like we’ve been using) with the user’s information.
  • Change the username column so that it can be clicked to fire an event that can be handled to display the user’s profile if desired. We’ll allow the person using the control to enable/disable this feature so that if they want, they username ISN’T highlighted. Disabled will be our default.

You’ll find actually, that much of what we do is pretty easy to add, I figured we’d have to make some hefty changes to some things to add extra columns, but in reality it turned out to be pretty easy, as we don’t actually call any of the columns by specific ordinal.

Solution

Online/Offline Indicator

The first thing we need to do for this column, is add another column to our gvManageUsers front-end code. We’ll make this the first column. So before the column for ‘Active’, add another TemplateFiled as such:

<asp:TemplateField HeaderText="Online" >
    <HeaderStyle HorizontalAlign="Center" />
    <ItemStyle HorizontalAlign="Center" />
    <ItemTemplate>
        <asp:Panel ID="pnlOnline" runat="server" />
    </ItemTemplate>
</asp:TemplateField>

We’re adding a column labeled ‘Online’. We make it centered and add a panel inside. We’ll use our code-behind to assign the CssClass property depending on the user’s online status. I’ll add a couple of classes to our style section as well that we’ll use as default styles for online and offline. Add the following:

.isOnline
{
    height: 10px;
    width: 10px;
    background-color: Lime;
    border: solid 1px black;
}
.isOffline
{
    height: 10px;
    width: 10px;
    background-color: Red;
    border: solid 1px black;
}

Finally, we’ll add the code behind to make it work. We’ll need to assign a CssClass to our panel as the row is created. First we’ll create the properties so that these CssClasses can be customized. Add the following to your code-behind:

Private _userOnlineCssClass As String = "isOnline"
Public Property UserOnlineCssClass As String
    Get
        Return _userOnlineCssClass
    End Get
    Set(ByVal value As String)
        _userOnlineCssClass = value
    End Set
End Property

Private _userOfflineCssClass As String = "isOffline"
Public Property UserOfflineCssClass As String
    Get
        Return _userOfflineCssClass
    End Get
    Set(ByVal value As String)
        _userOfflineCssClass = value
    End Set
End Property

They’re much the same as we’ve done before in this series, with the exception that we assign the values to privates variable rather than directly to the controls (as we’ll have to use them over and over and swap between the two).

Finally, we’ll add our code to the gvManageUsers_RowDataBound event to assign our CssClass. Add the following to your code to the gvManageUsrs_RowDataBound event handler:

If e.Row.RowType = DataControlRowType.DataRow Then
    Dim objUser As MembershipUser = CType(e.Row.DataItem, _
                                MembershipUser)
    Dim pnlOnline As Panel = e.Row.FindControl("pnlOnline")
    If objUser.IsOnline Then pnlOnline.CssClass = UserOnlineCssClass Else _
                                   pnlOnline.CssClass = UserOfflineCssClass
End If

If you’ve done any work with the RowDataBound events, you’ll know that we need to check to make sure the row is actually a data row before proceeding (rather than a header row or something). We’ll extract the user – it’s our DataItem, we bound the MembershipUserCollection to the GridView as the DataSource – and also get a reference to our panel. Then we’ll assign the correct CssClass to it depending on if the user is online or not.

If you run your project you’ll now see that we can now tell instantly if a user is online or not.

Unlocking the User Account

Next we’ll look at adding another column so that we can unlock a user’s account. We’ll create a checkbox much like our ‘Active’ column. The box will be checked if the account is locked, and allow us to uncheck it so the user is immediately activated. If the box is UNchecked, then we’ll make it read-only so that it cannot be changed as we can’t call a lockout method. To start with, we’ll add another column. I put mine right after the Active column. It is defined as follows:

<asp:TemplateField HeaderText="Unlock">
    <HeaderStyle HorizontalAlign="Center" />
    <ItemStyle HorizontalAlign="Center" />
    <ItemTemplate>
        <asp:CheckBox ID="chkLocked" runat="server" AutoPostBack="true"
                               OnCheckedChanged="UnlockUserAccount" 
     Checked='<%# DataBinder.Eval(Container.DataItem, "IsLockedOut") %>' 
     Enabled='<%# DataBinder.Eval(Container.DataItem, "IsLockedOut") %>'
      />
    </ItemTemplate>
</asp:TemplateField>

This is much like our Online/Offline column, with the exception that we’re using a checkbox rather than a panel. We will check the box depending on the IsLockedOut property of the user (which is our data item). We also make the enabled property dependant on the IsLockedOut property. Once the user is no longer locked out, it will automatically be read-only. Finally we need to call a subroutine when the checkbox is changed, we call UnlockUserAccount which we’ll need to define. Add the following to your code-behind:

Protected Sub UnlockUserAccount(ByVal sender As Object, _
                                                               ByVal e As EventArgs)
    Dim theCheckbox As CheckBox = CType(sender, CheckBox)
    Dim theItemRow As GridViewRow = _
                             CType(theCheckbox.Parent.Parent, GridViewRow)
    Dim theUsernameLabel As Label = _
                             CType(theItemRow.FindControl("lblUsername"), Label)
    Dim theUser As MembershipUser = _
                             Membership.GetUser(theUsernameLabel.Text)
    theCheckbox.Checked = Not theUser.UnlockUser()
    theCheckbox.Enabled = theCheckbox.Checked
End Sub

The process is a little convoluted: we pull the checkbox, get it’s parent’s parent (the row) and then find the label with the username. From that we pull the user and then unlock the account. The success of the attempt will determine the state of the checkbox, and the state of the checkbox will determine if the checkbox is enabled.

I figured I should raise an event at this point as well so that we could take extra action if so desired (like emailing the user). Let’s define an event at the top of our code-behind with our other events and then we’ll raise it here. Add the following event definition:

Public Event UserUnlocked(ByVal sender As Object, _
                                      ByVal e As UserEventArgs)

We’ll also add an entry to the UserEventAction enum in our UserEventArgs class so that we can user our UserEventArgs for EventArgs. Add the following to the enum:

UserUnlocked

Finally, we’ll raise our event in our UnlockUserAccount subroutine at the very end:

RaiseEvent UserUnlocked(Me, _
         New UserEventArgs(UserEventArgs.UserEventAction.UserUnlocked, _
                theUsernameLabel.Text))

User Information Display

I also decided that it would be nice to have a means of seeing some of the pertinent information regarding the user. There are a number of properties that we’ll display. Again, we’ll add another column to our gvManageUsers. My first thought was to add a little square like our online/offline with a ‘?’ in it for info, however this wasn’t very effective (ugly) and I decided to go with an ‘Info’ link in the last column. I also thought about making it appear on a MouseOver rather than having to click, but that meant adding a lot of javascript, when the rows are bound so I thought it better to do it on the fly with a click instead.

So add another column to our gvManageUser, defined as follows (sorry, it’s kind of ugly with it’s wrapping):

<asp:TemplateField>
    <ItemTemplate>
        <asp:LinkButton ID="lnkbtnInfo" runat="server" Text="Info"
             CommandName="ShowUserInfo" CommandArgument='<%# DataBinder.Eval(Container.DataItem, "UserName") %>' OnCommand="gridUsersClick" CausesValidation="false" />
    </ItemTemplate>
</asp:TemplateField>

Basically we’re just creating another link much the same as we did with any of our other links (such as delete/edit user). We’ll tie it back to our gridUsersClick just like the others but we’ll pass it different command information so we can differentiate it and have arguments to work with.

When we click the Info button, we’ll want to display a ModalPopup, so we’ll need to define that too. I added mine to the bottom of the page as follows (again sorry for the wrappings):

<%-- UserInfo dialog --%>
<asp:UpdatePanel ID="upnlUserInfo" runat="server" >
    <ContentTemplate>
        <asp:Panel ID="pnlUserInfo" runat="server" style="display: none;" CssClass="uacModalPopup">
            <asp:Table ID="tblUserInfo" runat="server">
                <asp:TableRow>
                    <asp:TableCell ColumnSpan="2" HorizontalAlign="Center">
                        <asp:Label ID="lblUserInfoTitle" runat="server" Font-Bold="true" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblStatusTitle" runat="server" Text="Is Currently Online: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblStatus" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblCreatedDateTitle" runat="server" Text="User Created On: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblCreatedDate" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblUserInfoEmailTitle" runat="server" Text="Email: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblUserInfoEmail" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblLastLoginTitle" runat="server" Text="Last Successful Login: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblLastLogin" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblLastActivityTitle" runat="server" Text="Last Activity: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblLastActivity" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblLastPasswordChangeTitle" runat="server" Text="Last Password Change: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblLastPasswordChange" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        <asp:Label ID="lblLastLockoutDateTitle" runat="server" Text="Last Lockout Date: " Font-Bold="true" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:Label ID="lblLastLockoutDate" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell ColumnSpan="2" HorizontalAlign="Center">
                        <asp:LinkButton ID="lnkbtnCloseUserInfo" runat="server" Text="Close" />
                    </asp:TableCell>
                </asp:TableRow>
            </asp:Table>
        </asp:Panel>
        <asp:Button ID="btnFakeMPEUserInfo" runat="server" Style="display:none;" />
        <cc1:ModalPopupExtender ID="mpeUserInfo" runat="server"
            TargetControlID="btnFakeMPEuserInfo"
            PopupControlID="pnlUserInfo"
            BackgroundCssClass="uacModalBackground" />
    </ContentTemplate>
</asp:UpdatePanel>

I know it looks like a lot, but really all it does is create a table with 7 different pieces of information we want to display about the user.

Now we just need to populate the fields and show it. We need to handle our click event in gridUsersClick. If you look in gridUsersClick, you’ll see when we handle other events, we usually run a Setup subroutine and then call SetUI, we’ll follow that pattern here. So before we modify our gridUsersClick we’ll add a SetupUserInfo subroutine. Add the following subroutine:

Private Sub SetupUserInfo(ByVal sUsername As String)
    Dim mu As MembershipUser = Membership.GetUser(sUsername)
    lblUserInfoTitle.Text = "Information on user: '" & sUsername & "'"
    lblStatus.Text = If(mu.IsOnline, "Yes", "No")
    lblLastActivity.Text = mu.LastActivityDate.ToString()
    lblLastLogin.Text = mu.LastLoginDate.ToString()
    lblLastPasswordChange.Text = mu.LastPasswordChangedDate.ToString()
    lblLastLockoutDate.Text = If(mu.LastLockoutDate.Year > 1800, _
                                                 mu.LastLockoutDate.ToString(), "Never")
    lblCreatedDate.Text = mu.CreationDate.ToString()
    lblUserInfoEmail.Text = mu.Email
End Sub

One item of note is that I use the If() function to check if the user is online, and return a friendlier ‘yes’ or ‘no’ answer.

We also need to add a little to our SetUI routine. Add the following to the SetUIModes Enum:

UserInfo

and add another case to our SetUI routine’s case statement:

Case SetUIModes.UserInfo
    mpeUserInfo.Show()

Finally, we’ll call SetUI from our gridUsersClick. To do that, we’ll add another if then statement to the end of the routine:

If e.CommandName.Equals("ShowUserInfo") Then
    SetupUserInfo(e.CommandArgument)
    SetUI(SetUIModes.UserInfo)
End If

Now if you run your project, you’ll have the ability to check the user’s information.

Enabling Editing a User’s Profile

I also wanted to enable the administrator to look at and possibly edit the user’s profile. I don’t want to add specific profile code into this control as the profile could change from site to site, rather what I want to do is enable the administrator to click a link, and then I’ll fire an event that can be handled to allow someone to display profile information however they want.

Also I want to allow this functionality to be enabled/disabled via property, that way it isn’t there if it hasn’t been explicitly wanted.

I’m going to do a little cheating. Rather than putting one control in our username column, I’m going to put two. I’m doing this for a couple reasons. First, we’re using our lblUsername in code already so removing it would require changing more code than I want to. Second, It’s easier to put two controls there and make them hide/reveal opposite of each other. So, we’ll start by modifying the column definition for our username column. Right now, it just has a label, we’ll modify the label by adding a visible attribute and then add a second control, a LinkButton. When finished, your TemplateField should look like this:

<ItemTemplate>
    <asp:Label ID="lblUsername" runat="server"
           Text='<%# Container.DataItem %>'
           Visible='<%# Not EnabledEditProfile %>' />
    <asp:LinkButton ID="lnkbtnUsernaem" runat="server"
              Text='<%# Container.DataItem %>'
              Visible='<%# EnabledEditProfile %>'
              CommandName="EditProfile" CommandArgument='<%# Container.DataItem %>'
              OnCommand="gridUsersClick"
             CausesValidation="false" ToolTip="Click to Edit Profile" />
</ItemTemplate>

The visible properties are set to be opposite of each other and will pull from the property we’ll define next in our back-end code. The label will be visible if editing profile is NOT enabled. We’ll call our gridUsersClick and pass in a different CommandName as well as the username as the CommandArgument.

We’ll add a property that we can use when using the control, and that will be checked to determine the visibility of our controls. Add the following to your backend code:

Private _enabledEditProfile As Boolean = False
Public Property EnabledEditProfile() As Boolean
    Get
        Return _enabledEditProfile
    End Get
    Set(ByVal value As Boolean)
        _enabledEditProfile = value
    End Set
End Property

When we handle the click, what we’ll do is raise an event that can be handled, so we need to define an event for when the edit profile is clicked. It isn’t so different that we can’t user our UserEventArgs. Add the following to your event section:

Public Event UserEditProfileClicked(ByVal sender As Object, _
                                                           ByVal e As UserEventArgs)

We’ll also add another line to our UserEventAction Enum in our UserEventArgs class:

EditProfileClicked

Now we just need to raise the event in our gridUsersClick subroutine. Add the following:

If e.CommandName.Equals("EditProfile") Then
    RaiseEvent UserEditProfileClicked(Me, New UserEventArgs _
              (UserEventArgs.UserEventAction.EditProfileClicked, _
                e.CommandArgument))
End If

Now, when you want to work with the user’s profile, you just need to your markup where you use this control and enable the profile functionality and handle the event.

Epilogue

I know I keep thinking I’ve reached the end of this control and functionality I want to add, so I won’t jinx myself by saying it is. However, that being said, I’m feeling pretty good about how it’s turning out and the functionality available in it. Hope you find it useful, I do.

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.

More Posts Next page »