Welcome to AspAdvice Sign in | Join | Help

.Net Discoveries

An attempt to pass along some answers I have discovered in my .Net coding.
Using the WinForm ComboBox Like the Asp.Net DropDownList

Prologue

Most of the development I do centers around ASP.Net rather than WinForms. I do however do a little work in the WinForms sphere occasionally. I was putting something together the other day and found that there are some little, yet significant differences between the WinForms ComboBox and the ASP.Net DropDownList. Specifically I found that in the ASP.Net DropDownList, you can set the displayed text to be different than the value. The WinForms ComboBox doesn't allow this.

Problem

In the application I was developing, I wanted the ComboBox to behave like the ASP DropDownList and allowing me to display text yet have the VALUE returned be an integer that corresponded with another array. WinForms DOES support this, HOWEVER, not in as straight forward a way as you might think. (In the interest of giving credit where credit is due, I pieced together much of this post by reading a post and subsequent comments on Martin G. Brown's blog - Thanks for the help Martin).

Solution

First, a little about the difference between ASP and WinForms ComboBoxes. Take the example here of an ASP.Net DropDownList's front-end code:

<asp:DropDownList runat="server" ID="ddlTest">
    <asp:ListItem Text="My Name" Value="0" />
    <asp:ListItem Text="Your Name" Value="1" />
</asp:DropDownList>

or the following back-end code for adding items to an existing DropDownList:

ddlTest.Items.Add(New ListItem("My Name", 0))
ddlTest.Items.Add(New ListItem("Your Name", 1))

In either case, notice that I can explicitly set the value and the text to be different. This allows me to display friendly text, yet pass around a key (such as an ID) when the user selects the entry rather than having to use the text as a key. Also notice that I can set both of these at the same time. This would allow me to pull a list of objects, say names from a database, and then display them along with associated values (such as a primary key id) pretty quickly. Let's look at doing this with the WinForms combo box.

Let's do a little setup here so we can continue with our project when we're done "looking." Create a Windows Form application and add a ComboBox (name it cmboDoIt) and two Buttons (one named btnDoIt and one named btnShowIt).

Click on the ComboBox, go to the properties pane and click the ... button next to Items, you'll notice that the String Collection Editor comes up. It asks you to enter the strings for the collection, one per line. So you can enter the text, but not the values. Let's try doing it runtime and see if that'll do it.

If you go to the code behind and try adding an item, like you did for the DropDownList. Notice that the Item that you add is of type Object. But of what kind of object? String. This doesn't allow you to add a value to the item. So how could we do this? The answer lies in the ability of a ComboBox to be bound to a data source. If we create a data source with more than one "column", we can bind to the data source and then SPECIFY which column will be the text and which will be the value.

To do this, we first need to create a data source. We'll do this by creating a class to hold our two data pieces. Add the following class definition to our form, just before the final End Class tag:

Public Class ValueDescriptionPair
   Private _value As Object
   Private _description As String

   Public Sub New(ByVal Description As String, ByVal Value As Object)
      _value = Value
      _description = Description
   End Sub

   Public ReadOnly Property Value() As Object
      Get
         Return _value
      End Get
   End Property

   Public ReadOnly Property Description() As String
      Get
         Return _description
      End Get
   End Property

   Public Overrides Function ToString() As String
      Return _description
      End Function
End Class

Basically we create a class that can hold both our value and our description. We add a ToString function so that we don't end up with any funky output when we use our description. We also define a constructor so we can pass our value/description pair in to create the object. Now we can create an array of our ValueDescriptionPair objects, load it with data (we'll just do it by hand in this example) and then bind it all to our ComboBox. To do this, add the following to our btnDoIt_Click event handler.

Dim vdpArray As New ArrayList

vdpArray.Add(New ValueDescriptionPair("My Name", 0))
vdpArray.Add(New ValueDescriptionPair("Your Name", 1))
cmboDoIt.DataSource = vdpArray
cmboDoIt.ValueMember = "Value"
cmboDoIt.DisplayMember = "Description"

Here we create an ArrayList of our ValueDescriptionPair. We then add 2 elements, bind our DataSource and then define which member to assign as a value and which to assign as a description. Let's wire up one last piece before we run the application. Add the following to your btnShowIt_Click event handler:

MessageBox.Show("The value for " & cmboDoIt.SelectedItem.ToString & _
   " is " & cmboDoIt.SelectedValue)

This will just display your selected entry and the corresponding value. Now to use the value, all you need to do is use the ComboBox's SelectedValue property and you're good to go.

Note: as an FYI, this can also be done using a generic collection. Simply change the following lines (remember to add the Imports System.Collections.Generic statement to the top of the code as well):

Dim vdpArray As New ArrayList
Dim vdpArray As New List(Of ValueDescriptionPair)

Epilogue

There you have it. Pretty simple once you've got it all in place, but very convenient when needed. I'm not sure why you can't add data items that contain both value and description like you can with the DropDownList but it's an easy work around.

Using the AJAX Control Toolkit’s CollapsiblePanel in a GridView

Prologue

On our company website, the one I mention so frequently (or is it infrequently as often as I post…), we have a data display prominently mounted where it can be viewed by people who are not employees. If they visit our facility, they can view the data (in a GridView) that shows basic information they want to know. The information that feeds the dataset has quite a bit more information that may actually be helpful to various departments within the company, but we don't want to display it to non-employees. What I’d like to do is retain the same GridView but reveal more data when someone in the company clicks the row. Basically, I want to have every other row hidden and then reveal an associated, hidden row when the non hidden row is clicked. (FYI - I also have code I don't talk about in this post that disables this functionality for people who are not inside the company).

Problem

A possible solution that I found was the CollapsiblePanel control that is part of the AJAX Control Toolkit. This provides functionality to do pretty much what I was looking to do. The question is trying to integrate it with a GridView. In my searching on the Internet, I didn’t find much that would suffice for what I wanted to do. One of the closest I found was a kind of an integrated master/detail that I found on a blog. I chose to go ahead and try to use the CollapsiblePanel control. The problem that I found is that you can’t alternate data in a GridView, the row is the row and nothing else. In other words, I found I couldn’t do the 2 things I wanted to with the row 1. hide it (every other row should be hidden), and 2. put different data in it automatically (i.e. different data in every other row, one with public info, one with private). I did get it working, but it's not a wonderfully clean solution, not quite like I expected. Let’s examine how I did it.

Solution

To get started we need to create a new page and add to it a GridView control, it also needs a little configuration so, add the following markup code to your front-end code:

<cc1:ToolkitScriptManager ID="ToolkitScriptManager1" runat="server">
</cc1:ToolkitScriptManager>
<asp:gridview ID="gvwTest" runat="server" AutoGenerateColumns="true"
    AllowSorting="True">
</asp:gridview>

We've just created a very basic GridView, nothing much to look at. We also added a ScriptManager for our CollapsPanelExtender to use later. Next we'll create a data source to populate the GridView. Add the following to your Page_Load event:

gvwTest.DataSource = CreateDataSource()
gvwTest.DataBind()

And then we'll add a helper function CreateDataSource to our application so that it will create and populate the data source,  add the following function:

Public Function CreateDataSource() As DataTable
    Dim dt As New DataTable()
    dt.Columns.Add(New DataColumn("FirstName", GetType(String)))
    dt.Columns.Add(New DataColumn("LastName", GetType(String)))
    dt.Columns.Add(New DataColumn("City", GetType(String)))
    dt.Columns.Add(New DataColumn("State", GetType(String)))

    Dim dr As DataRow
    dr = dt.NewRow()
    dr("FirstName") = "Sally"
    dr("LastName") = "Smith"
    dr("City") = "Phoenix"
    dr("State") = "Arizona"
    dt.Rows.Add(dr)
    dr = dt.NewRow()
    dr("FirstName") = "John"
    dr("LastName") = "Doe"
    dr("City") = "Seattle"
    dr("State") = "Washington"
    dt.Rows.Add(dr)

    Return dt
End Function

This function creates a DataTable and then adds 4 columns, 2 for the name and 2 for their location. Next, we create 2 rows and populate the data. Finally, we return the DataTable.

Now we're ready to get started. If you run your project, you'll see that we have 2 rows each with 4 columns. Let's say though that you'd rather have the location be on the next row and have it revealed only when you click on the name. How do we get 1/2 of one row to display as the 1st row and the 2nd half of the SAME row to display as the 2nd row? The answer is that you can't. If we want to create something custom like that, we need to create a custom TemplateField and this will have to be laid out by hand, I chose to do it by table.

So let's customize our GridView. We'll add support for the collapse panel at the same time. Let's start with how we want to lay it out. We want one row with the full name. We don't want the location to show until the name is clicked, and then we want it to "fly-out" (or down) and reveal the location. What we'll end up with is a table with two rows, one row with name one with location. This table will be nested in EACH row of our GridView. Let's get started by creating a header for our GridView. Add the following between our GridView tags:

<Columns>
    <asp:TemplateField>
        <HeaderTemplate>
        </HeaderTemplate>
    </asp:TemplateField>
</Columns>

We'll be using one actual GridView field, typically each field (in our data) would map to a cell in the row, but we're going to use one cell per GridView row to display our data. Within our HeaderTemplate we'll add a table labeling the data that will be displayed. Add the following inside your HeaderTemplate:

<asp:Table runat="server">
    <asp:TableRow>
        <asp:TableCell>First Name</asp:TableCell>
        <asp:TableCell>Last Name</asp:TableCell>
    </asp:TableRow>
</asp:Table>

That sets the header for the GridView, now for our row. We need to add two tables, one for each "row" we want to present (one for name, one for location). We'll also surround our tables each with a panel so we can target them with the CollapsePanelExtender. All this will be added to our ItemTemplate, add the following just below our HeaderTemplate:

<ItemTemplate>
    <asp:Panel ID="pnlTitle" runat="server">
        <asp:Table runat="server">
            <asp:TableRow>
                <asp:TableCell>
                    <asp:Image ID="imgToggle" ImageUrl="expand.jpg" runat="server" />
                </asp:TableCell>
                <asp:TableCell>
                    <asp:label ID="lblFirstName" Text='<%#Eval("FirstName")%>' runat="server" />
                </asp:TableCell>
                <asp:TableCell>
                    <asp:Label ID="lblLastName" Text='<%#Eval("LastName")%>' runat="server" />
                </asp:TableCell>
            </asp:TableRow>
        </asp:Table>
    </asp:Panel>
    <asp:Panel ID="pnlLocation" runat="server">
        <asp:Table runat="server">
            <asp:TableRow>
                <asp:TableCell>
                    <asp:Label ID="lblCity" Text='<%#Eval("City")%>' runat="server" />
                </asp:TableCell>
                <asp:TableCell>
                    <asp:Label ID="lblState" Text='<%#Eval("State")%>' runat="server" />
                </asp:TableCell>
            </asp:TableRow>
        </asp:Table>
    </asp:Panel>
</ItemTemplate>

You'll notice that I reference an image called 'expand.jpg' this is a little "expander" arrow. The CollapsePanelExtender will actually toggle this image with a 'collapse.jpg' that we'll define later. You can grab any expander and collapser image you like and just direct our code to it (I'm using the ones from the toolkit's sample project). If you run your project, you'll see that our first column has our tables in it and our other columns still appear. We can turn those off by changing our AutoGenerateColumns to false. If you run it again, we should have our tables and no extraneous columns.

Now that we've got everything in place, we just need to get our CollapsPanelExtender working.

We'll need a CollapsePanelExtender for each row that we'll be targeting so we should add the extender inside our ItemTemplate and put it directly after our last panel tag. We'll start with just the basics and add a couple things in a minute. Add the following just below your last panel tag:

<cc1:CollapsiblePanelExtender ID="cpeDetails"
runat="server"
TargetControlID="pnlLocation"
CollapseControlID="pnlTitle"
ExpandControlID="pnlTitle"
Enabled="true" >
</cc1:CollapsiblePanelExtender>

Now if you run your project, we'll have our two "rows" and if you click on the name row, the location row will shrink and if you click it again, it will reveal. Lets modify a couple settings to make it a little better. First let's add functionality for the expander image so it changes automatically, add the following attributes to your cpeDetails extender:

ImageControlID="imgToggle"
CollapsedImage="Collapse.jpg"
ExpandedImage="expand.jpg"
Collapsed="true"

This will set the CollapsPanelExtender to do image swapping and make will make the location panels collapsed by default. One other change to make would to alternate colors for the rows, add the following attribute to your GridView control:

AlternatingRowStyle-BackColor="Silver"

Run it again, and see that it works, we have one "row" with the name and another "row" with the location that can be revealed by clicking on the name.Notice that each row can be expanded and contracted separately from the other rows and that everything is in working order with the collapse and expand images. There we go, a working 'Collapsible' table from a GridView.

Epilogue

You'll notice that I said earlier, this isn't a really clean way to do things, I'll stand by that. The header doesn't necessarily match up with our data, and we're limited on what we can do about that. You can set some cell widths and cell alignments in our tables so that they move where they should be, but you only have so much control over a table, it might expand to fit contents and stuff like that.

However, it is a cheap and easy way to display some extra information pretty easily, and it does look reasonably good in our production environment.

A Provider by Any Other Name…Doesn’t Smell So Sweet

Prologue

A funny thing happened on the way to the… management page. I found a kind of an anomaly taking place when you configure your Membership Provider use one application and your Role Provider is configured to use a different one. What I found is that I had users I deleted still assigned to some of my roles (phantom users).

Problem

In the process of creating ‘A Better ASP.Net User/Role Management Page’ found that I (without realizing it) had set the application name for my Roles Provider to be different than the one for the Membership Provider. This leads to some interesting side effects that I didn’t expect. Since I haven’t found any posts regarding this on the internet, I thought I’d touch on the subject mostly as an interesting side note so people can see what’s going on, not that we solve any problems per se but we’ll be informed at least by the time we’re done.

Solution

Before we can get started, we’ll need to make sure we have a website setup with a Membership and a Role provider configured. We should be able to piggyback off the SQLExpress Membership Provider website that was created in the last post. We’ll want to follow the post and create a Membership provider that we can configure. We’ll scrap all the front end and back-end code from the project except the web.config. We’ll also need to define a Role provider while we’re at it so after you add your Membership provider information, add the following as a Role provider right below it:

        <roleManager enabled="true" defaultProvider="myCustomRoleProvider">
            <providers>
                <add name="myApp"
             connectionStringName="mySqlServer" 
             type="System.Web.Security.SqlRoleProvider, System.Web,
                       Version=2.0.0.0, Culture=neutral, 
                       PublicKeyToken=b03f5f7f11d50a3a"/>
            </providers>
        </roleManager>

As a side note, make both your membership and roles provider’s applicationName=”myApp”, so that we start with our providers using the same application. Along the same lines of the project we’re piggybacking on, this will allow us to customize our role provider using the ASPNETDB database that was provided to us. Next we’ll want to create a front end that will allow us to show the results we’re creating as we go, so we know what’s happening.

Open a blank default.aspx (or create it if it doesn’t already exist) and add the following to the front-end code:

<div>
    Roles by User:<br />
    <asp:Label ID="lblRolesByUser" runat="server" Text="" />
    <br />
    <br />
    Users by Role:<br />
    <asp:Label ID="lblUsersByRole" runat="server" Text="Label" />
</div>

This give us two labels to populate, one we’ll populate with our users and show what roles they are assigned to, and the other we’ll populate with the roles and the users that are assigned to them. Now for the back-end code. Add the following helper functions to the code behind:

Private Function GetRolesByUser() As String
    Dim sb As New StringBuilder()
    For Each member As MembershipUser In Membership.GetAllUsers()
        sb.Append("<strong>" & member.UserName & "</strong><br>")
        For Each role As String In Roles.GetRolesForUser(member.UserName)
            sb.Append("-" & role & "<br>")
        Next
    Next
    Return sb.ToString()
End Function

Private Function GetUsersByRole() As String
    Dim sb As New StringBuilder()
    For Each role As String In Roles.GetAllRoles()
        sb.Append("<strong>" & role & "</strong><br>")
        For Each member As String In Roles.GetUsersInRole(role)
            sb.Append("-" & member & "<br>")
        Next
    Next
    Return sb.ToString()
End Function

These functions do exactly what we were talking about before. Once cycles through the members in the database, retrieves the roles they are assigned to and then displays this list. The other cycles through the roles and makes a list of each role and all the members assigned to it then displays it. To run the functions, add the following to your Page_Load event handler:

lblRolesByUser.Text = GetRolesByUser()
lblUsersByRole.Text = GetUsersByRole()

This will populate the labels we have on the page using our two helper functions as soon as the page is run. Now were’ ready to do some playing around. Let’s make sure that to start, you have your membership and roles providers both using the same application name. This way we can see how things SHOULD behave. Add the following line to both your membership and your role provider:

applicationName="myApp"

For First My First Trick – or – Meet the Phantom User

Now let’s go add some users and roles and make some assignments. If you open the solution explorer panel and click ‘ASP.Net Configuration’ button, we’ll get the ‘ASP.net Web Site Administration Tool’ (WSAT – well call it) webpage. Let’s add 2 users and 2 roles and then assign 1 user to each role. For my purposes, I’ll use:

Users Roles
MajorPain BigManOnCampus
JunkMeister LittleLoser

and just assign roles directly across a row. Once we have our users and roles setup, we can run our application and we should get a listing that looks like this:

Roles by User:
JunkMeister
-LittleLoser
MajorPain
-BigManOnCampus

Users by Role:
BigManOnCampus
-MajorPain
LittleLoser
-JunkMeister

You can see that we have our 2 users, 2 roles and 2 assignments (each user is assigned to one role). All this is displayed to us two different ways. Now delete one of your users (I got rid of MajorPain) and refresh the page. Your results should look something like this:

Roles by User:
JunkMeister
-LittleLoser

Users by Role:
BigManOnCampus
LittleLoser
-JunkMeister

Notice that we have our 1 user, and that the user we deleted was removed from the roles list as well, we have 1 user, 2 roles, and only 1 user assigned. This is what we would expect to see. If we remove a user, the provider code should remove its entry from all the roles assignments.

Now let’s get a little fancy with things and see what happens. First, let’s change the name of the membership provider. In your Web.config, change the membership provider’s applicationName to be something different:

applicationName="myOtherApp"

Now refresh your page and see what gets listed. It should look something like this:

Roles by User:

Users by Role:
BigManOnCampus
LittleLoser
-JunkMeister

We have 0 users and 2 roles, but we do have 1 user assigned to our role… Wait a minute, how can we have a user assigned to our LitteLoser role if we don’t have any users? We’ll answer that in a minute. For now, let’s look at it the other way around, change our membership’s applicationName back to “myApp” and change the Roles provider’s applicationName to “MyOtherApp”. Refresh your page and see that it should come up like this":

Roles by User:
JunkMeister

Users by Role:

Notice that we have our user back, but we don’t have any roles in our application AND that since there aren’t any roles in our application, our user isn’t assigned to any… It seems there a bit of a disconnect? We get roles with a phantom user assigned, but not any phantom roles assigned for our user. Interesting, No?

A Good Magician Never Reveals – or – I am Not a Good Magician

Ok, let’s dig into what’s happening and see how it all works, change your membership and role’s applicationName back to myApp and setup with your 2 users, 2 roles and 2 assignments again. If you’ve got it setup right, your page should display as follows:

Roles by User:
JunkMeister
-LittleLoser
MajorPain
-BigManOnCampus

Users by Role:
BigManOnCampus
-MajorPain
LittleLoser
-JunkMeister

Now, let’s dig into the behind the scenes. Open your ‘Server Explorer’ panel. Open your database, then the tables node and let’s look into our tables. First, right-click your Users table and select ‘Show table data’. You should see 2 users, like the picture to the right.

Let’s also dig into the Roles table, show the table data for the roles, and you should see a table with 2 roles, again nothing surprising (see image at right).

Ok, let’s look at one more table, show the table data for your UsersInRoles table. You should see two assignments (two rows). While the IDs may look intimidating, we can see from our pictures above that the UserID comes from the Users table and the RoleID comes form the Roles table. Thus we can translate our IDs and see that we have our 2 users, 2 roles and 2 assignments.

Now, let’s repeat our previous experiment, but let’s start with the Role provider. Change your Role Provider’s applicationName and refresh our browser, we should get:

Roles by User:
JunkMeister
MajorPain

Users by Role:

2 users, 0 Roles and 0 role assignments, as it should be. Where did our roles go? For that we need to open one more table, show table data on our Applications table. This should show our ApplicationID’s notice how we have two applications, now notice that our roles and our users both have an applicationID as well. Both These all have an applicationID that matches our ‘myApp’ application. Meaning that when we change our Roles provider to to be ‘myOtherApp’, it no longer detects that we have any roles or role assignments since it looks for roles and role assignments only with our applicationID (Note: not strictly correct as we’ll see shortly). This allows you to use the same database for more than one website, each website just needs a different applicationID and the data resides in the same database.

Now let’s try it with our Membership Provider, change the Roles Provider’s applicationName back to ‘myApp’ and our Membership Provider’s applicationName to ‘myOtherApp’. Now refresh our page and see what happens. If it works like when we changed our Roles provider, we we would expect to end up with 2 Roles, 0 users, and 0 role assignments, as all the users assigned to roles exist in ‘myApp’ and we’ve ask for users in ‘myOtherApp’. Refresh your page and you should get:

Roles by User:

Users by Role:
BigManOnCampus
-MajorPain
LittleLoser
-JunkMeister

We should be expecting 0 users, 2 roles and 0 assignments, but Notice that we’re getting usernames that we SHOUDN’T be getting: 0 users, 2 roles and 2 assignments. What gives? Check out our code behind. In our ‘GetUsersByRole’ function, we’re getting all our roles, iterating through them and using the role to pull all members in the role by using the Roles.GetUsersInRole(role) method. For some reason, this built-in function has some kind of glitch where it doesn’t filter out users that aren’t in our specified application (myOtherApp). That’s the secret of the trick: we should be getting only users returned to us who are IN our specified application (myOtherApp), but in fact we’re getting back users that WE SHOULD NOT BE ABLE TO SEE by virtue of our configuration.

We can see part of the root of the problem by looking at our UsersInRoles table. Notice something missing? Yeah, it’s got no applicationID. When we use the Roles.GetUsersInRole, it should process the UserID against the applicationID (from a different table) and do a filter. It doesn’t.

To confirm that we’re getting roles assignments (and thus usernames) from multiple applications, right now add another user (MickyMouse) and assign him to the ‘BigManOnCampus’ role and refresh your page. You should see something like this:

Roles by User:
MickyMouse
-BigManOnCampus
Users by Role:

BigManOnCampus
-MajorPain
-MickyMouse
LittleLoser
-JunkMeister

We only have 1 user in our users list, but we have a total of 3 users to roles assignments AND we’ve pulled usernames from TWO DIFFERENT APPLICATIONS AT THE SAME TIME and used them!

I Think You Underestimate My Trickiness – or – Different appNames From the Get Go

Ok, now let’s get a little more hairy with it. While we’re here, delete your MickeyMouse user. and then refresh your page. Since we’re in the application where we created our MickyMouse user, the user should be deleted and removed from all roles, right? Refresh and see, you should see something like this:

Roles by User:

Users by Role:
BigManOnCampus
-MajorPain
-MickyMouse
LittleLoser
-JunkMeister

We’ve got 0 users, but 3 users assigned to roles. Technically, we’re pulling two of the users from a different application, so they DO exist in the database, but our MickeyMouse user no longer exists in the Database or he would show up in the user section (he’s not being filtered out, we’re in his application). What gives?

We’re finally getting down to the crux of the problem I originally found. I had accidentally created an applicationID for each the Role and the Membership, and didn’t even notice till I pulled the data we are here that little things were being left floating around (like our MickeyMouse assignment).

Let’s see if we can create the issue from the ground up and watch what’s happening in our database so we can see how it happens. Rather than going through the interface of the WSAT, I’ll have you delete directly from the tables since we won’t be able to 100% flush our users out otherwise (since MickyMouse doesn’t exist yet still shows up).

If you select all your rows in VS.Net, while ‘showing table data’, you can press delete and delete all the rows. Let’s do this for the following tables and in this order (no cheating, no looking at the data in the tables, now):

UsersInRole
Roles
Membership
Users

The order of tables here will give a little bit of hint of things to come, (hint: we haven’t mentioned the membership table at all, have we). Also, in preparation for our next excercise we want to make sure that our Membership and Roles providers have DIFFERENT applicationNames. Ok, now if you run your page again, you should end up with a blank list:

Roles by User:

Users by Role:

This is to be expected since we don’t have any users or roles in our database. So, now that we have everything setup, let’s add 2 users and 2 roles, but NOT make any assignments just yet. Open your WSAT and add back in our MajorPain and JunkMeister users and add back in our BigManOnCampus and LittleLoser roles. Refresh your page and you should see:

Roles by User:
JunkMeister
MajorPain

Users by Role:
BigManOnCampus
LittleLoser

2 users, 2 roles and 0 assignments – as expected. First, let’s look into our tables. Let’s start with UsersInRoles. Notice we get nothing (no assignments yet). Now, let’s check our Roles and Users tables. You shouldn’t be surprised to see that each has 2 entries, and also shouldn’t be surprised that the roles have a different ApplicationID than the users do.

Ok, now let’s add a users to a role. Start with MajorPain, add him to the BMOC role and then refresh our page. We should see our assignment:

Roles by User:
JunkMeister
MajorPain
-BigManOnCampus

Users by Role:
BigManOnCampus
-MajorPain
LittleLoser

Now let’s look back at our UsersInRole table and see what it looks like. Much like we’d expect, one row showing our one assignment. Now, let’s go back and look at our users table.

WHOA. Where did the extra MajorPain come from? Well, since we didn’t have a MajorPain in our Role’s Application, the database creates one in our users table and assigns it the ApplicationID for our Roles provider. Notice that we have two MajorPains, but they do have 2 different ApplicationIDs assigned to them (one for our membership provider’s application and one for our role provider’s application).

Now, observe what happens when we delete our user MajorPain. Go ahead and delete him. What happened? Right, only one disappeared. Notice it was the one associated with our Membership provider. Our Roles provider’s MajorPain user is still intact. Refresh our page and see what happens.

Roles by User:
JunkMeister

Users by Role:
BigManOnCampus
-MajorPain
LittleLoser

We have no MajorPain user, but we have a phantom MajorPain in our role assignments list. Thus we created a 2nd MajorPain user and now we’ve stranded him in our database. And we can’t just change our membership provider to match his applicationID and delete him. Change your Membership provider’s ApplicationName to match your Roles Provider and refresh your page. Theoretically, he should show up as a user right? WRONG! When you refresh, you get:

Roles by User:

Users by Role:
BigManOnCampus
-MajorPain
LittleLoser

No users, but we do get our phantom user to role assignment. What gives? Go back to our Membership table and take a look. We only have one row in this table, it belongs to JunkMeister (match is UserID up with the User’s table). But if you go to the User’s table and look you’ll see our phantom MajorPain user. Try to lookup our phantom MajorPain’s UserID, it isn’t in the Membership table. Unless the user has an entry in this table (Membership), it won’t show up when we use our membership methods. Even removing our ‘phantom’ user from the role doesn’t remove our phantom user from the Users table.

Epilogue

So there it is, phantom users in our membership/roles database.

Funny, I started out this post with the intent to create an innocently intentioned post about something strange that I found. However as I worked my way through presenting it, I found I was kind of disillusioned with the innocence. While I’m generally pretty happy with Microsoft and their related .Net technologies, it seems that this would be considered a fairly LARGE security loophole and/or could cause problems for any one of a number of reasons:

  • By changing the role provider’s application name, I can (if they exist) retrieve usernames that belong to a different application. While it might be assumed that I wouldn’t be able to access more than one application without proper permission, this seems somewhat lacking in security. (Especially when you consider some of the functionality that wasn’t baked into the providers for security’s sake – such as changing a username – it seems that this wasn’t a deliberate oversight).
  • Along with the above problem, I can, by changing the role provider’s application name, pull username/role assignments from MULTIPLE applications AT ONE TIME. Again, this seems like somewhat of a security problem – and I’ll get INCORRECT data that may give me hiccups in my code.
  • I can retrieve usernames and role associations that no longer exist. This poses a problem if I want to process things using information from the roles list. I may not be able to manipulate things based on the Roles.GetUsersInRoles() method, because it returns INCORRECT data and may thus return some unexpected exceptions.
  • We can create phantom users in our users table, however, we CANNOT, through the providers mechanisms provided by Microsoft, purge the phantom user(s) entirely from the database.
  • And on top of it all, I wasn’t able to find ANY documentation anywhere regarding the issue. Maybe nobody else is dumb enough to split their providers (like I was), but it seems like if it was designed to behave this way, that it would be explained somewhere in documentation or at least on a blog somewhere. At least tell us it’s possible, but warn us that it’s stupid a bad idea.

Anyhow, as I see it “Lucy, you got some splainin’ to do!” – or – At the very least, we should be very careful to make sure that we don’t mix up our applicatioNames when we are configuring our providers, or we may get some unexpected and undesirable results.

Configuring Your SQLExpress Membership Provider

Prologue

In the process of writing the last series of posts to create ‘A Better ASP.Net Member/Role Management Page’ (here’s a link to the 1st part), I used a SQL Express database located in my App_data folder. This was the first time I’ve used one. I have access to a full blown external SQL Server for development (since that’s what I’ll be doing in production). Due to my lack of experience with this form of database, I had a heck of time figuring out how to configure my membership provider. In part 4 of the series, I looked at enabling/disabling some functionality of the page based on how the Membership provider was configured. Obviously, to check that the functionality of the page is working correctly, I needed to change the configuration of the provider. Therein lied the problem.

Problem

When I created my database in the App_Data folder for this project, VS.Net 2008 did a lot of the work for me. While this isn’t bad, what I found is that I had a dickens of time figuring out how to get my Membership provider configured to use something besides the default settings. So I thought I’d create an entry on how to do it since I had a tough time with it.

We’ll start from the very beginning (i.e. from scratch) for this post, but the basic problem is that when VS.Net does all the work, the web.config does NOT contain any membership provider configuration information, so it isn’t readily apparent how to change the provider’s configuration. Once we figure out what’s going on and how to rectify it, it actually becomes pretty easy though. (Note, giving credit where credit is due, I got a lot of information from this site, but have pared it down to the very basics).

Solution

First off, let’s start with a new website. We only need one page in the site, it comes with a default.aspx and we’ll just use that. We’ll begin by setting up the page to display a couple things. Add the following to your front-end code:

<div>
   Configuration: <br />
   <asp:TextBox ID="txtCurrentConfig" runat="server"
       TextMode="MultiLine" Width="400" Height="200" />
   <br /><br />
   Members:<br /> 
   <asp:GridView ID="gvMembers" runat="server"> 
   </asp:GridView>
</div>

We’re creating a TextBox to hold some configuration information that we’ll retrieve in our back-end, and then a GridView that we’ll use to pull members from the database. This will help us to see if we’ve done something horribly wrong ;-).

We’ll need also to create our database before we can pull any membership from it. Right-click your App_Data folder and select ‘Add New Item…’ From this list, you want to select SQL Server Database. Name it ‘aspnetdb.mdf’ and click ‘Add’. Now we should have a database file in our App_Data folder. If you go to the Server Explorer panel, you can open the database, you’ll notice that it doesn’t have ANY tables in it, including the membership and roles tables that we want to be able to access. We need to add these.

Adding the membership and roles information basically amounts to running the aspnet_regsql.exe against our database and the the functionality will be added to the database. The article I referenced above has a great walkthrough on this, however I found that if I ran the aspnet_regsql tool in GUI mode, somehow the SQL service on my machine didn’t have rights to access the DB file (as it was in my My Documents folder under the VS 2008 folder, and somehow the GUI – despite being run under my account – couldn’t see my profile). Also, I couldn’t get my SQL Server to attach to the file for the same reason. What I did find that worked though, was an article that detailed how to run aspnet_regsql by command line (hey, and the article’s got a classy color scheme too ;-) ). This is how I’ll do our database. Open a command prompt and run the following:

aspnet_regsql -A all -C "Data Source=.\SQLEXPRESS;Integrated Security=True;User Instance=True" -d "C:\<Filepath>\APP_DATA\aspnetdb.mdf"

You should notice that if you have the filepath to your database file correct, it will add a few a few different things to your database and then report it is finished. If you refresh your database in the Server Explorer, you’ll see that now we have some tables (and stored procedures). We’re ready to do some stuff with our membership provider now.

Next, we’ll add a user to our database so that we can do some testing using our GridView. We’ll do it the easy way, open the Website Administration Tool by clicking the button on the toolbar in your Solution explorer. Once the page comes up, Click the ‘Security’ link. Click the link to ‘Select Authentication Type’, and we’ll set it to forms authentication. Select ‘From the internet’ and click ‘Done’. Also, we’ll click the link to ‘Enable Roles’. Now, let’s add a user to our database, click the ‘Create User’ link. On the next screen, let’s create a user, just to prove a point, use ‘password’ for the password. When you click ‘Create User’. You’ll notice that the user creation fails because we don’t have a non-alphanumeric character in the password. Go ahead and create a new password that meets the criteria and then create the user.

Ok, now we’ll add some back-end code so we can see what’s happening with our provider. Add the following to your page_load event handler:

gvMembers.DataSource = Membership.GetAllUsers()
gvMembers.DataBind()

txtCurrentConfig.Text = "Password format:" & Membership.Provider.PasswordFormat

What we’re doing is retrieving all our members, and binding them to the GridView. We also pull the PasswordFormat specified for the membership provider and display that in our textbox. If you run your page, you’ll see that we successfully pull our lone member into the GridView, and that our PasswordFormat is 1. This PasswordFormat correlates with the enumeration for PasswordFormats that is defined in the .Net Framework as follows (ok, I’m interpreting what they’ve done but it still stands):

Enum MembershipPasswordFormat
    Clear = 0
    Encrypted = 2
    Hashed = 1
End Enum

You can see that the 1 indicates that we have Hashed passwords. Herein lies our dilemma (yes, we’re finally to it). Let’s say we wanted to use Encrypted passwords instead? (I needed to change this so I could test the different PasswordFormats against the code in the aforementioned post). Normally, we’d just go and change the Membership provider’s details in the web.config. So let’s go do it. Go look in your web.config for the membership provider’s definition… go on, I’ll wait. Haven’t found it? Keep looking. While you’re at it, see if you can find the connectionString for the membership database… Can’t find it either? There must be one, otherwise how would it connect to the database? You’re right, we’ve mysteriously created a provider we don’t know how to configure.

The solution lies in this: our Machine.config has a default membership provider defined. When we created our membership stuff, this default provider is used by… default. We have to override this if we want to customize it. So how do we do this? First, let’s open our machine.config and see if we can find our Membership provider’s definition there. The machine.config can be found at C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config (assuming you’re %system% folder is Windows). Search down and you’ll find that there is in fact a provider defined here:

<membership>
    <providers>
        <add name="AspNetSqlMembershipProvider"
           type="System.Web.Security.SqlMembershipProvider, System.Web,
                   Version=2.0.0.0, Culture=neutral,
                   PublicKeyToken=b03f5f7f11d50a3a"
           connectionStringName="LocalSqlServer"
           enablePasswordRetrieval="false"
           enablePasswordReset="true"
           requiresQuestionAndAnswer="true"
           applicationName="/"
           requiresUniqueEmail="false"
           passwordFormat="Hashed"
           maxInvalidPasswordAttempts="5"
           minRequiredPasswordLength="7"
           minRequiredNonalphanumericCharacters="1"
           passwordAttemptWindow="10"
           passwordStrengthRegularExpression=""/>
    </providers>
</membership>

There it is. The current definition of our membership provider. So how do we override it? Simple, we need to define a new one in our web.config code. Somewhere inside your system.web section of the web.config, add a membership provider section and define a new provider as such:

<membership>
  <providers>
    <add name="myCustomProvider"
         type="System.Web.Security.SqlMembershipProvider, System.Web,
               Version=2.0.0.0, Culture=neutral,
               PublicKeyToken=b03f5f7f11d50a3a"
         passwordFormat="Encrypted"
         connectionStringName="LocalSqlServer" />
  </providers>
</membership>

Notice that we changed our passwordFormat to be Encrypted (so when we run the application, our format should now show as 2. How do we know what connectionStringName to use? We’ll, for one we could just use the same string as the one defined in the provider in the machine.config, but we can also look through the machine.config and see that we have a connectionString defined there too.

<connectionStrings>
    <add name="LocalSqlServer"
       connectionString="data source=.\SQLEXPRESS;Integrated
           Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User
           Instance=true"
       providerName="System.Data.SqlClient"/>
</connectionStrings>

You’ll see, that the default connectionString setup in the machine.config is ‘LocalSqlServer’. We could redefine it, but this will work just fine for us. So run your application and see what we get. The member binds fine to the GridView, but we’re still showing that our password is hashed (1). What’s the deal? Well, what’s happening, is we’re still using the default provider in the machine.config. We need to specify that we want to use ours instead. to do this, modify your membership element opening tag with the following attribute addition:

<membership defaultProvider="myCustomProvider">

Here we define that OUR provider should be the default provider. We also can take an additional step recommended my many people on the internet, and that is to remove the default provider from the machine.config. To do that, you can just add the clear element before your <add> provider element as follows:

<providers>
          <clear/>
          <add name=”…..

Alternatively, we could also remove the provider specifically by using the following in place of the <clear/> element:

<remove name="AspNetSqlMembershipProvider" />

We’re simply telling .Net in to remove the default provider (named AspNetSqlMembershipProvider). Now run your application again. Notice that our passwordFormat is now 2 – Encrypted. BUT, also notice that we no longer are successfully binding our GridView to the member we created. What gives? Add the following line to the end of your page_load event handler and we’ll see:

txtCurrentConfig.Text &= vbCrLf & "Application Name:" & _
   Membership.Provider.ApplicationName

Now run your application again. Notice that the applicationName is the name of your website. If you look in your machine.config, what is the applicationName being used? Right, “/” or root. Effectively, by leaving out the applicationName in our membership provider definition, we’ve changed to a different application. Our user is assigned to the root application, and we’re pulling members from a different application (our website name). To fix that, just add the following to your provider’s definition:

applicationName="/"

Run your application again, and you’ll see that now we have our member back. Or better yet, we could create a unique application name for our application and start creating users in that instead. This helps to ensure that our users are unique to our application and not being reused from some other application that used the root application name.

Epilogue

Well, there it is. Now you can configure your SQLExpress membership provider as you desire. There are a load of other settings that we didn’t define in ours (as witnessed by the settings in the machine.config) and defining them is easy as adding the settings.

Funny how something so simple could cause me so much searching all over the internet looking for the solution. Honestly, it took me quite a long time to find the answer, I probably wasted 2 hours or more before I stumbled into the answer. Maybe I’m just dense, it and it should be common knowledge, but it wasn’t to me.

Anyhow, it’s there, and if either of us ever need it, we’ll know where to look for the answer.

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

Prologue

**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**

In previous posts, we created “A better ASP.Net Member/Role Management Page”. If you want to follow along, you can create the management page yourself by reviewing all the posts for part 1, part 2, part 3, part 4, part 5 and part 6. In this post, I plan on ‘tying up some loose ends,’ by adding some things that I’d like to have in the management page, but that were over and above basic functionality. Most of this is stuff that I thought of adding while I was creating the page originally, but didn’t want to add yet, or that I didn’t have time to research at the time. Now's my chance to do it.

Problem

Time to add some ‘fluffy stuff’ and add some stuff that will make it just that little bit better… (and I found now that I’ve written this post, add some corrections as well…)

Solution

To get started, let's make it easier to customize the look and feel of the user control.

CssClass Properties

The first thing we’ll look at adding will give you a little more flexibility as far as styling your control. Currently it should look pretty decent but, it looks like Microsoft’s theme, and not our site (I don’t know about yours, but ours doesn’t look like Microsoft’s). I’m sure that we’ll change our site again someday, so it doesn’t make sense to hard code the styles and not have the option to change the styles on the fly. Lucky for us it should be reasonably easy to build in this functionality. I won’t cover how to do every one of them, but if we do it right we should be able to cover a bunch in a couple of groups, and the basics of how it can be done should give you a significant start.

Creating our own styles for the page doesn’t mean we have to remove the styling that we’ve done so far. We’ll just give the user the ability to override our styles to fit their needs. I’ve compiled a list of all the places that I would like to give the user the ability to define the CssClass (I think I got them all, I may have missed some…). We’ll start with the easy ones and show how it’s done and then let you loose on the others. First off, we’ve got three groups of classes that we can set the whole group all at once, our TextBoxWaterMarkExtenders watermark class, our ModalPopupExtender’s background class (the cover for the existing page) and our Panels that will be our modal dialogs (the actual panels being modally displayed). Each of these groups can each be set at once since they’ll be the same (all the watermarks will be the same for instance). Let’s start with our TextBoxWaterMarkExtenders. Here’s the list of controls I compiled:

tweSearchUsersFor
tweSearchUsersForRoles
tweSecurityAnswer (note, this may be misspelled in other posts as tbeSec…)
tweNewUsername (note, this may be misspelled in other posts as tbeNew…)
tweNewRoleName

What we’ll do is create a property for the WaterMarkTextCssClass and then set all their WaterMarkCssClasses when the property is set. Add the following to your back-end code:

Private _TextBoxWatermarkCssClass As String
Public Property TextBoxWatermarkCssClass() As String
    Get
        Return _TextBoxWatermarkCssClass
    End Get
    Set(ByVal value As String) 
        _TextBoxWatermarkCssClass = value
        tweSearchUsersFor.WatermarkCssClass = value
        tweSearchUsersForRoles.WatermarkCssClass = value
        tweSecurityAnswer.WatermarkCssClass = value
        tweNewUsername.WatermarkCssClass = value
        tweNewRoleName.WatermarkCssClass = value
    End Set
End Property

I cheated, I use the Refactor! tool, that I’ve mentioned before, so all I had to do to create the property is write the declaration line and then right click on it and select and refactor. I then added all the TextBoxWaterMarkExtender controls from the list to the set portion of the property and assigned the value passed in to their WatermarkCssClass property. Now go to our test page that tests our user control and in the HTML markup that uses the control, add an attribute for WatermarkCssClass and assign it a css class with different style than the one we’re using (my css class had one line: color: red;). Run it and you’ll see that it overrides the one that we assigned in the control’s front end markup. (Note, I’ve found that when you add properties this way, you may need to close and reopen the page that’s using the control otherwise it may not show our new property as an intellisense available attribute).

One other small note: if you notice I saved the property to a global variable. Realistically, we could just put the value to the controls and then call it good, but if we want to pull it and look at later, it then it might be nice to pull it from a central point rather than from one of the controls.

Next, we’ll do the ModalPopupExtender BackgroundCssClass. The ModalPopupExtenders that I cataloged are:

mpeCreateUser
mpeMessageDialog
mpeDeleteUserConfirm
mpeEditUser
mpeChangePassword
mpeConfirmResetPassword
mpeChangeSecurityQuestion
mpeChangeUsername
mpeDeleteRoleConfirm
mpeChangeRoleName
mpeRoleRenameConfirm

To create their property, add the following to your back-end code:

Private _ModalPopupBackgroundCssClass As String
Public Property ModalPopupBackgroundCssClass() As String
    Get
        Return _ModalPopupBackgroundCssClass
    End Get
    Set(ByVal value As String)
        _ModalPopupBackgroundCssClass = value
        mpeCreateUser.BackgroundCssClass = value
        mpeMessageDialog.BackgroundCssClass = value
        mpeDeleteUserConfirm.BackgroundCssClass = value
        mpeEditUser.BackgroundCssClass = value
        mpeChangePassword.BackgroundCssClass = value
        mpeConfirmResetPassword.BackgroundCssClass = value
        mpeChangeSecurityQuestion.BackgroundCssClass = value
        mpeChangeUsername.BackgroundCssClass = value
        mpeDeleteRoleConfirm.BackgroundCssClass = value
        mpeChangeRoleName.BackgroundCssClass = value
        mpeRoleRenameConfirm.BackgroundCssClass = value
    End Set
End Property

Nothing much different than we did before, just more controls to assign to. Again, add an attribute to your control declaration on the test page and see the difference if you run it (my popup background is orange now :-0 ). Finally for the easy ones, let’s do the panels that will be displayed as the ModalPopup. The list I compiled are:

pnlMessageDialog
pnlDeleteUserConfirm
pnlCreateUser
pnlEditUser
pnlChangePassword
pnlConfirmResetPassword
pnlChangeSecurityQA
pnlChangeUsername
pnlDeleteRoleConfirmation
pnlChangeRoleName
pnlChangeRoleNameConfirm

And one more time add the property and assign the values. Add the following to the backend code:

Private _ModalPopupCssClass As String
Public Property ModalPopupCssClass() As String
    Get
        Return _ModalPopupCssClass
    End Get
    Set(ByVal value As String)
        _ModalPopupCssClass = value
        pnlMessageDialog.CssClass = value
        pnlDeleteUserConfirm.CssClass = value
        pnlCreateUser.CssClass = value
        pnlEditUser.CssClass = value
        pnlChangePassword.CssClass = value
        pnlConfirmResetPassword.CssClass = value
        pnlChangeSecurityQA.CssClass = value
        pnlChangeUsername.CssClass = value
        pnlDeleteRoleConfirmation.CssClass = value
        pnlChangeRoleName.CssClass = value
        pnlChangeRoleNameConfirm.CssClass = value
    End Set
End Property

Check it if you want, my control is staring to look pretty odious (I’m choosing some truly awful colors, just for kicks).

For the remaining CssClasses, we have a number of other one-off’s that we’ll need to address individually. I won’t be addressing them in this post, but I’ll list them and then you can add the property code to assign their classes just as we did above. The controls I cataloged were:

tblSearchUsers
tblSearchUsersHeader
tblSearchUsersBody
gvManageUsers – CssClass, RowStyleCssClass, AlternativeRowStyleCssClass
tblCreateUser
tblCreateRole
tblCreateRoleHeader
gvRoles – CssClass, RowStyleCssClass, AlternativeRowStyleCssClass
tblSearchForUsersRole
tblSearchForUsersRoleBody
gvUsersForRole – CssClass, RowStyleCssClass, AlternativeRowStyleCssClass

Two notes: 1. Some of these can likely be combined (such as the GridView’s CssClasses) since I want them to look the same I’ll set them the same time, 2. If you go back trough your code, you’ll notice that the tables and the Header and Body elements here listed (i.e. tblSearchUsers and tblSearchUsersHeader), may need to have an ID attached to them so we can call them in the back-end code (they may not already have ID’s). This means that you’ll be adding an ID to a table (tblSearchUsers) and to a TableCell (tblSearchUsersHeader – as it isn’t truly a header, it’s a TableCell masquerading as one) in your front-end code so you can call it directly in the back-end. Then we’ll create a property that calls it a header (i.e. headerCssClass) – so even some child elements will have ID’s assigned. Create and assign the properties as you did above.

Realistically, any CSS that we created in the front-end, you can setup as a property and override as you desire.

Real-time Username Notification

One of the things I thought would be really great to have from the get go was to add real-time username availability notification. What I mean by that is for those locations where the admin creates or changes the username, allow the dialog to inform the admin whether the username is available AS they are typing. Something akin to this (and the actual demo here).

When I started this project, the authors weren’t fully finished with develpment, but they did finish by the time I finished our control. Unfortunately (in our instance), they created a user control. BUT, wanting to put my control together as a self-contained usercontrol, I didn’t want to add another external control to the mix. I looked into reusing the code and integrating it into my control, and it could be done. What I didn’t think about though is that the front-end page needs to call to something, via Javascript/AJAX as you are typing. Any way it is sliced, (at least from what I found on the site) this requires a web service of some kind on the back-end (that’s the something). Even if I can call a function contained in the same page (which the control alas is NOT, it’s a control not a page), it has to be declared as a web service so that it can be called by the AJAX (there’s an interesting article on using jQuery to call a page method that I read).

To get it to work, the jQuery has to call a page, by name, and then the method (we could pass the page name to the control by property in the front-end and write it into our javascript code so the jQuery can call it (I’ve written on injecting into javascript before). We also would have to enable the page to support web services, which I gather necessitates some changes to the web.config. This then means that we’re not self contained. We have already broken our self contained requirement by adding functionality for changing the username (and will again in the next section), but realistically, to get this functioning, wasn’t worth the time I thought I’d end up putting in. The admin will get notification that the username is not available, it just won’t be while typing.

Role Descriptions

Here’s another interesting bit of functionality that Microsoft left out (and I can’t discern why). Let’s say we want to put in a description of the role so that the admin can go back and remember what each role is meant for (the roles I’m using are descriptive to departments here at the company, but not necessarily to what rights the user gets). Descriptions would be handy. What to do? Well if you examine the roles table in the database, you’ll see that Microsoft conveniently added a description field to the database (nvarchar 256). So it seems that we’d be able to pretty easily enter a description. NOPE. Apparently, this is unimplemented functionality. There is nothing in the role provider to facilitate this. If we want to do it, we’ll have to again break our self-contained directive, and add some database functionality. We can create a property that we’ll use to specify whether the external functionality has been created like we did with our Change Username functionality. But, alas it means breaking the self-contained directive AGAIN.

I’m going break our directive anyway, I believe it will have enough use for me that I’ll go ahead and break our rules again. First we need to start with modifying the front-end code. We want to add another column to our Manage Roles GridView. Add the following to the definition of gvRoles. Add it between the RoleName and 'Assign Users’ columns (as the 2nd column):

<asp:TemplateField HeaderText="Description" Visible="false">
    <HeaderStyle HorizontalAlign="Left" />
    <ItemTemplate>
       <asp:label runat="server" ID="lblRoleDescription" 
        Text="<%# GetRoleDescription(Container.DataItem.ToString()) %>" />
        (<asp:LinkButton runat="server" ID="lnkRoleDescription"
              ForeColor="Black" Text='Edit'
              CommandName="EditRoleDescription"
              CommandArgument='<%# Container.DataItem.ToString() %>' 
              OnCommand="RoleLinkButtonClick"/>)
    </ItemTemplate>
</asp:TemplateField>

You’ll notice that we setup another column for the description, it is composed of 2 parts, a label for the description, and an edit link (you may notice that the control for that is surrounded by parentheses, that’s not a typo, they’re string literals for the HTML). You’ll also notice that we call a function GetRoleDescription. We’ll have to define that in our back-end code later. We also set the column’s visible property false so by default it is NOT shown. I put two controls in our column, one that displays the description name, and one that displays an edit link.

Originally, I thought to put the description functionality with the change role name functionality, however I rethought that, as the change role name is already a special circumstance. We’ll keep them separate which means we’ll add another dialog for changing the description. As for adding a description when the admin creates a role, since the description functionality is not out of the box, I’ll force the admin to create a new role and then take a second step to enter a description.

We’ll want to create a new modal dialog for editing our role description. Add the following to the bottom of our front-end code:

    <%-- Change Role description Dialog --%>
<asp:UpdatePanel id="upnlRoleDescription" runat="server"
   ChildrenAsTriggers="true">
    <ContentTemplate>
        <asp:Panel ID="pnlRoleDescription" runat="server"
             style="display: none;" CssClass="modalPopup"
             HorizontalAlign="Center">
            Enter a description for the '<asp:label id="lblRoleDescriptionName"
                runat="server" Text="" />' role.<br /><br />
            <asp:Table ID="Table1" runat="server">
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                        Role description:
                     </asp:TableCell>
                    <asp:TableCell HorizontalAlign="Left">
                        <asp:TextBox ID="txtRoleDescription" runat="server" />
                        <cc1:TextBoxWatermarkExtender ID="tweRoleDescription"
                              TargetControlID="txtRoleDescription"
                              WatermarkText="Enter role description."
                              WatermarkCssClass="watermarked" runat="server" />
                    </asp:TableCell>
                </asp:TableRow>
            </asp:Table>
            <br />
            <asp:LinkButton ID="lnkRoleDescriptionSave"
                CausesValidation="true" runat="server" Text="Save" />&nbsp;
            <asp:LinkButton ID="lnkRoleDescriptionCancel" runat="server"
                 Text="Cancel" />
        </asp:Panel>
        <asp:Button ID="btnFakeMPERoleDescription" runat="server"
              style="display: none;" />
        <cc1:ModalPopupExtender id="mpeRoleDescription" runat="server"
            TargetControlID="btnFakeMPERoleDescription"
            PopupControlID="pnlRoleDescription"
            BackgroundCssClass="modalBackground" />
    </ContentTemplate>
</asp:UpdatePanel>

It’s pretty standard for our dialogs. We have one TextBox, for the description and two LinkButtons for save and cancel. In keeping with previous dialogs, let’s display this using our SetUI subroutine. Add the following to our SetUI enumeration:

EnableRoleDescription
RoleDescriptionDialog

Then we’ll add some functionality to our SetUI subroutine. Add the following two cases to your SetUI case statement:

Case SetUIModes.EnableRoleDescription
    gvRoles.Columns(1).Visible = True
Case SetUIModes.RoleDescriptionDialog
    mpeRoleDescription.Show()

This just takes care of displaying things. The first case will make our column visible if the property is set to true. The second shows the dialog. We’ll also need to add some functionality to our RoleLinkButtonClick event handler. Add the following If statement to your RoleLinkiButtonClick:

If e.CommandName.Equals("EditRoleDescription") Then
    lblRoleDescriptionName.Text = CStr(e.CommandArgument)
    txtRoleDescription.Text = GetRoleDescription(e.CommandArgument)
    SetUI(SetUIModes.RoleDescriptionDialog)
End If

This will set the textbox to the current role description and then show the edit dialog when the admin clicks the ‘Edit’ link.

Next, let’s setup the save and cancel buttons in the role description dialog. Add the following to your back-end code to your lnkRoleDescriptionCancel_Click event handler:

txtRoleDescription.Text = String.Empty

This simply clears our TextBox if we cancel so we don’t have any loose ends. Next add the following to our lnkRoleDescriptionSave_Click event handler:

If UpdateRoleDescription(lblRoleDescriptionName.Text, _
        txtRoleDescription.Text) = True Then
    lblMessageDialog.Text = "Role description successfully updated."
    SetUI(SetUIModes.MessageDialog)
    gvRoles.DataSource = Roles.GetAllRoles()
    gvRoles.DataBind()
Else
    lblMessageDialog.Text = "Changing the role's description was _
         unsuccessful."
    SetUI(SetUIModes.MessageDialog)
End If

Here we call a UpdateRoleDescription subroutine and pass it the role’s name and the new description. If it returns successful, we’ll rebind the GridView with new data, and display a success message. If it fails, we’ll display a failure message. In our SQL code, we will be retrieving the role by the role’s name. Since we won’t be allowed by our provider to add a role name more than once per ApplicationID (more on that later), we should be ok doing it this way.

We’ll need to define some SQL before proceeding. We’ll create two stored procedures for our database. First, let’s define the GetRoleDescription’s stored procedure:

CREATE PROCEDURE aspnet_Roles_GetRoleDescription
    @RoleName nvarchar(255),
    @ApplicationName nvarchar(255),
    @RoleDescription nvarchar(255) OUTPUT
AS
DECLARE @ApplicationID nvarchar(255)
SELECT @ApplicationID = ApplicationID FROM aspnet_Applications
   WHERE ApplicationName = @ApplicationName

SELECT @RoleDescription = Description FROM aspnet_Roles
   WHERE RoleName = @RoleName AND ApplicationID = @ApplicationID

First off, we create three SQL variables, two will be input and one output. The input variables will pass in our role name and our application name. We’ll use the application name to pull the ApplicationID and then use the role name and ApplicationID to target the description. We need to include the ApplicationID since we can create two roles with the same name in the database IF they have different applications assigned to them. If we target by role name only, we could be effecting multiple roles, especially when we get to updating the role’s description. Finally we’ll retrieve the description and assign that to our output variable.

Next, we’ll define our UpdateRoleDescription stored procedure as follows:

CREATE PROCEDURE aspnet_Roles_UpdateRoleDescription
    @RoleName nvarchar(255),
    @ApplicationName nvarchar(255),
    @RoleDescription nvarchar(255),
    @Results bit OUTPUT
AS
DECLARE @ApplicationID nvarchar(255)
SELECT @ApplicationID = ApplicationID FROM aspnet_Applications
   WHERE ApplicationName = @ApplicationName

UPDATE aspnet_Roles SET Description = @RoleDescription
   WHERE RoleName = @RoleName AND ApplicationID = @ApplicationID

SET @Results = @@ROWCOUNT

This time we’re creating three input variables, since we need to pass in the new description as well as the role name and application name. Much like the previous stored procedure, we’ll use the application name to retrieve the ApplicationID and then use that to target the correct role. Then we update the description for the role and return the affected row count as a result. This way, we can check the success of our operation. We should only get a 1 back (meaning one row affected). If we get anything else, we’ll have failed.

Now we’re ready to do some hookups in the back-end code. First, we’ll define our GetRoleDescription function. Add the following function to your back-end code:

Public Function GetRoleDescription(ByVal sRoleName As String, _
       Optional ByVal iOrdinalForConnectionString As Integer = 0) As String  

  If _RoleDescriptionFunctionality = _
           RoleDescriptionSettings.DbIsSetup Then

    Using cn As New SqlConnection(ConfigurationManager.ConnectionStrings
      (iOrdinalForConnectionString).ConnectionString)
        Dim cmd As New SqlCommand("aspnet_Roles_GetRoleDescription", cn)
        cmd.CommandType = Data.CommandType.StoredProcedure
        cmd.Parameters.Add("@RoleName", _
              Data.SqlDbType.NVarChar).Value = sRoleName
        cmd.Parameters.Add("@ApplicationName", _
              Data.SqlDbType.NVarChar).Value = Roles.ApplicationName
        cmd.Parameters.Add("@RoleDescription", Data.SqlDbType.NVarChar, _
              255).Direction = Data.ParameterDirection.Output
        cn.Open()
        Try
            cn.Open()
            cmd.ExecuteScalar()
        Catch ex As Exception
            Return string.empty
        Finally
            cn.Close()
        End Try
        If cmd.Parameters("@RoleDescription").Value.Equals _
                  (System.DBNull.Value) Then
            Return String.Empty
        Else
            Return cmd.Parameters("@RoleDescription").Value
        End If
    End Using
  Else
    Return String.Empty
  End If

End Function

In the parameter list for our function, you’ll notice we pass in our role name, and we also pass in an optional parameter for our connectionstring. If you remember back to part 5 in the series, we added some database functionality for changing the username. I mentioned when we created the back-end code for ChangeUsername, that this was perhaps the least flexible part of our entire project, since we can’t automatically pull the current connectionstring from anywhere. We need to have a way to retrieve it so we can create a connection to our database though. Since the connectionstrings are in an array, we can specify them by ordinal number. In my simple application here at work, my connectionstring is 0 (the only one). To target another connectionstring for our connection, we would need to determine the ordinal number of the connectionstring and pass it in. (UGLY I agree… excuse me while I hurl…). We may be able to set this via property as we did above with cssClasses so that we don’t have to modify the code to change the connectionstring.

The function creates a connection, and then a command object. We load the command object up with our parameters. Nothing fancy, except one item of note, we specify a size for the return parameter. If we don’t specify the size, our application will return an error about not allowing a string of size 0. We’ll then open the connection and try to execute. We’ll use a try catch and if we get an error, we’ll return an empty string. If we’re successful, we’ll check to see if we got a null value returned. If so, we’ll again return an empty string. If it’s NOT null, we’ll return the description.

Next, let’s define our UpdateRoleDescription function:

Public Function UpdateRoleDescription(ByVal sRoleName As String, _
      ByVal sNewDescription As String, _
      Optional ByVal iOrdinalForConnectionString As Integer = 0) As Boolean

Using cn As New SqlConnection(ConfigurationManager.ConnectionStrings _
       (iOrdinalForConnectionString).ConnectionString) 
    Dim cmd As New SqlCommand("aspnet_Roles_UpdateRoleDescription", cn)
    cmd.CommandType = Data.CommandType.StoredProcedure
    cmd.Parameters.Add("@RoleName",_ 
       Data.SqlDbType.NVarChar).Value = sRoleName
    cmd.Parameters.Add("@ApplicationName", _
       Data.SqlDbType.NVarChar).Value = Roles.ApplicationName
    cmd.Parameters.Add("@RoleDescription", _
       Data.SqlDbType.NVarChar, 255).Value = sNewDescription
    cmd.Parameters.Add("@Results", _
       Data.SqlDbType.Bit).Direction = Data.ParameterDirection.Output
    cn.Open()
    Try
        cmd.ExecuteNonQuery()
    Catch ex As Exception
        Return False
    Finally
        cn.Close()
    End Try
    Return cmd.Parameters("@Results").Value
End Using
End Function

Here we’re passing in three parameters, our role name, our new description name, and again our odious connectionstring ordinal. We create a connection and a command, load it up with our data, and then execute the query. If it fails, we return false, if it runs, we’ll return the result (if it fails, it’ll be false that gets returned).

Viola, now we can add role descriptions. Why Microsoft hasn’t bothered to make this part of the Roles provider I’ll never know. I mean why build it into the database if you aren’t going to use it? Anyhow, Role descriptions at our disposal.

Fixing Change Username Functions

In the process of adding role description functionality, I found that I didn’t properly execute our ChangeUsername function. If you remember, in the above section, we passed in the applicationName so that we target only the Role that we want to. Technically, we can have more than one username or more than one role in a table if we add it using a different applicationName. I didn’t take that into account when I put together ChangeUsername. I neglected to use the application name SO, technically the code would change ALL the usernames rather than just the one we want. Let’s make some changes so that it behaves properly. Let’s modify our ChangeUsername function to include an applicationName parameter. Add the following just below our @newUsername parameter in our ChangeUsername function:

cmd.Parameters.Add("@applicationName", _
   Data.SqlDbType.NVarChar).Value = Membership.ApplicationName

And then we’ll need to modify our aspnet_Membership_ChangeUsername stored procedure. The SQL to update the stored procedure is as follows (changes are in bold):

ALTER PROCEDURE aspnet_Membership_ChangeUsername
    @currentUsername nvarchar(255),
    @newUsername nvarchar(255),
    @applicationName nvarchar(255),
    @results bit OUTPUT
AS

DECLARE @ApplicationID nvarchar(255)
SELECT @ApplicationID = ApplicationID FROM aspnet_Applications
   WHERE ApplicationName = @ApplicationName
 

    UPDATE aspnet_Users
    SET UserName = @newUsername,
        LoweredUserName = LOWER(@newUsername)
    WHERE LOWER(UserName) = LOWER(@currentUsername)
        AND ApplicationID = @ApplicationID

    set @results = @@ROWCOUNT

There, now we properly target our specific username.

One other thing I want to update is to reset the change username dialog when the admin clicks cancel. To do this, all we need to do is add two lines to our lnkChangeUsername_Click event handler. Add these lines right after the End If line, and before the lblCurrentUsername.text line:

txtNewUsername.Text = String.Empty
divUserAvailablity.InnerText = String.Empty

Previously, if we canceled changing the username, the the new username textbox and an error message, if applicable, would be there when we reopened it. Now it won’t, it will be pristine like it’s supposed to be.

Epilogue

And there it is, “A Better Asp.Net Member/Role Management Page”. There were a couple other tings I thought about adding to the mix, like rewriting all the confirmation dialogs so that there is one common dialog that we could reuse everywhere we confirmed things, but I didn’t really want to go through the trouble of rewriting the whole mess just to reduce the code, it wasn’t worth it.

If anyone has something that they added, that they’d like to tell us about, drop me a line and I’ll put it in the comments section. I’m sure we’d all love to see it.

But for me, that’s it for the management page. I’ll be putting it into production now that I’m done writing it (yes, I was still finishing it), and unless there are any issues that people point out or errors that need corrected. I’m calling it good and being done! PHEW!

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

Prologue

**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**

In previous posts, we started creating “A Better ASP.Net Member/Role Management Page.” In part 1, we defined our criteria, and in part 2, part 3, part 4 and part 5 we created functionality to do all our user maintenance. In this section, we work on our role maintenance functionality.

Problem

We actually have a fair amount of things to do to maintain roles. We’ll create a grid much like the one for the user list, for the roles. We’ll also create a user GridView, so when we select a role, we can search the users and assign them to the given role (much like the user search functionality that we’ve already created. We’ll also need to create a number of dialog boxes that we can popup to get information or confirmation from the administrator. Once we’re finished, we’ll have functionality to add/remove, assign and change role names.

Solution

First things first. We need to create some user interface portions. Add the following two sections to your front-end code somewhere between our pnlFormatEverything tags:

<%-- Manage Roles --%> 
   <asp:UpdatePanel ID="upnlManageRoles" runat="server"
              ChildrenAsTriggers="true"> 
       <ContentTemplate> 
           <asp:Panel ID="pnlManageRoles" runat="server" Visible="false"> 
               <asp:Label ID="lblManageRolesTitle"
                  runat="server" Text="" /><br /> 
               <asp:Label ID="lblManageRolesInstructions" runat="server"
                  Text="Select <b>Edit Role</b> to view or change the
                  settings for the roles." /> 
               <br /><br /> 
               <asp:Table ID="tblCreateRole"
                  CssClass="tableBorders" runat="server"> 
                   <asp:TableRow> 
                       <asp:TableCell CssClass="tableHeadersNTitles"> 
                           <asp:Literal ID="lblCreateRoleTile" runat="server"
                              Text="Create New Role:" /> 
                       </asp:TableCell> 
                   </asp:TableRow> 
                   <asp:TableRow> 
                       <asp:TableCell> 
                           <asp:Label ID="lblCreateRole" runat="server"
                              Text="New Role Name:" /> 
                           <asp:TextBox runat="server" ID="txtNewRoleName"/> 
                           <cc1:TextBoxWatermarkExtender ID="tweNewRole"
                              TargetControlID="txtNewRoleName"
                              WatermarkCssClass="watermarked"
                              WatermarkText="Enter new role name"
                              runat="server" />
                           <asp:Button id="btnAddRole" runat="server"
                              Text="Add Role"/> 
                       </asp:TableCell> 
                   </asp:TableRow> 
               </asp:Table> 
               <br /> 
               <asp:GridView runat="server" CellPadding="3" 
                      GridLines="Horizontal" 
                      ID="gvRoles" 
                      CssClass="userGridView" 
                      AutoGenerateColumns="false" 
                     AllowPaging="true" 
                      PageSize="7" 
                      UseAccessibleHeader="true"> 
                   <RowStyle CssClass="gridRowStyle" /> 
                   <AlternatingRowStyle CssClass="gridAlternatingRowStyle" /> 
                   <PagerStyle CssClass="gridPagerStyle" /> 
                   <PagerSettings mode="Numeric" /> 
                   <HeaderStyle CssClass="tableHeadersNTitles"
                        Font-Bold="true" /> 
                   <Columns> 
                      <asp:TemplateField HeaderText="Role Name"> 
                         <HeaderStyle HorizontalAlign="Left" /> 
                         <ItemTemplate> 
                            <asp:Label runat="server" ID="lblRoleName"
                              ForeColor="Black"
                              Text='<%# Container.DataItem.ToString() %>' /> 
                          </ItemTemplate> 
                      </asp:TemplateField> 
                      <asp:TemplateField> 
                         <HeaderStyle HorizontalAlign="Center" /> 
                         <ItemTemplate> 
                            <asp:LinkButton ID="lnkManageUsersInRole"
                                 runat="server" Text="Assign Users"
                                 CommandName="EditUsersInRole"
                                 CommandArgument='<%#
                        Container.DataItem.ToString() %>' ForeColor="Black"
                                  OnCommand="RoleLinkButtonClick" /> 
                         </ItemTemplate> 
                      </asp:TemplateField> 
                      <asp:TemplateField> 
                         <ItemTemplate> 
                            <asp:LinkButton ID="lnkDeleteRole" runat="server"
                              Text="Delete Role" CommandName="DeleteRole" 
                    CommandArgument='<%# Container.DataItem.ToString() %>'
                              ForeColor="Black"
                              OnCommand="RoleLinkButtonClick" /> 
                         </ItemTemplate> 
                      </asp:TemplateField> 
                      <asp:TemplateField> 
                         <HeaderStyle HorizontalAlign="Left" /> 
                         <ItemTemplate> 
                             <asp:LinkButton ID="lnkEditRoleName" runat="server"
                                Text="Change Role Name"
                                CommandName="EditRoleName" 
                    CommandArgument='<%# Container.DataItem.ToString() %>'
                                 ForeColor="Black"
                                 OnCommand="RoleLinkButtonClick" /> 
                         </ItemTemplate> 
                      </asp:TemplateField> 
                   </Columns>
                </asp:GridView>
                <asp:Label runat="server" ID="lblNoRoles" Visible="false"
                     Text="No roles have been created." />
                <br /><br />
                <asp:LinkButton ID="lnkManagerUsers"  runat="server"
                     Text="Manager Users" />
            </asp:Panel>
        </ContentTemplate>
    </asp:UpdatePanel> 


   <%-- Assign/Remove Users from Role --%> 
   <asp:UpdatePanel ID="upnlAddRemoveUsersFromRole"
            runat="server" ChildrenAsTriggers="true"> 
      <ContentTemplate> 
         <asp:Panel ID="pnlAddRemoveUsersFromRole" runat="server"
                  Visible="false"> 
            <asp:Label ID="lblAddRemoveUsersFromRoleTitle" runat="server"
                  Text="" /><br /> 
            <asp:Label ID="lblAddRemoveUsersFromRoleInstructions"
               runat="server" 
               Text="Use this page to manage the members in the specified role. To add a user to the role, search for the user name and then select User Is In Role for that user. " /> 
            <br /><br /> 
            Role:
            <b>
               <asp:Literal runat="server" ID="lblRoleToAddRemoveUsersTo" />
            </b> 
            <asp:Table id="tblSearchForUsersRole" runat="server"
                  CssClass="tableBorders">
               <asp:TableRow> 
                  <asp:TableCell>
                     <asp:Literal ID="lblSearchForUsersRole" runat="server"
                        Text="Search For Users" />
                     </asp:TableCell> 
                  </asp:TableRow> 
                  <asp:TableRow>
                     <asp:TableCell CssClass="tableBody">
                        <asp:Label id="lblSearchForUsersRoleSearchBy"
                           runat="server" Text="Search By: " /> 
                        <asp:DropDownList ID="ddlSearchForUsersRole"
                              runat="server"> 
                           <asp:ListItem id="item3" Text="Username"
                              runat="server" /> 
                           <asp:ListItem id="item4" Text="Email" runat="server" /> 
                        </asp:DropDownList> 
                        &nbsp;
                        <asp:Label ID="lblSearchForRoles"
                           runat="server" Text="for: " /> 
                        <asp:TextBox id="txtSearchUsersForRoles"
                           runat="server" />&nbsp; 
                        <cc1:TextBoxWatermarkExtender
                           ID="tweSearchUsersForRoles"
                           TargetControlID="txtSearchUsersForRoles"
                           WatermarkCssClass="watermarked"
                           WatermarkText="Enter criteria" runat="server" />
                        <asp:Button id="btnSearchUsersForRoles"
                           runat="server" Text="Search For" /> 
                        <br /> 
                        <asp:Label runat="server" ID="lblWildcardAllowed"
                           Text="Wildcards * and ? are permitted." />
                        <br /> 
                        <asp:Repeater runat="server"
                           ID="rptAlphabetRepeaterRoles" >
                           <ItemTemplate>
                              <asp:LinkButton runat="server" id="lnkLetterRoles"
                                 CommandName="Display"
                                 CommandArgument="<%# Container.DataItem %>"
                                 Text="<%# Container.DataItem %>" />&nbsp; 
                            </ItemTemplate> 
                        </asp:Repeater> 
                     </asp:TableCell> 
                 </asp:TableRow> 
             </asp:Table> 
             <br /> 
             <asp:GridView runat="server" CellPadding="3"
               GridLines="Horizontal" ID="gvUsersForRole"
               CssClass="userGridView" AutoGenerateColumns="false"
               AllowPaging="true" PageSize="7" UseAccessibleHeader="true">
                  <RowStyle CssClass="gridRowStyle" /> 
                  <AlternatingRowStyle CssClass="gridAlternatingRowStyle" /> 
                  <PagerStyle CssClass="gridPagerStyle" /> 
                  <PagerSettings mode="Numeric" /> 
                  <HeaderStyle CssClass="tableHeadersNTitles"
                        Font-Bold="true" /> 
                  <Columns> 
                     <asp:TemplateField HeaderText="Username"> 
                        <HeaderStyle HorizontalAlign="Left" /> 
                        <ItemTemplate> 
                           <asp:Label ID="lblUsernameRole" runat="server"
                              ForeColor="Black"
                              Text="<%# Container.DataItem %>" /> 
                        </ItemTemplate> 
                     </asp:TemplateField> 
                     <asp:TemplateField HeaderText="User is in role"> 
                        <HeaderStyle HorizontalAlign="Center" /> 
                        <ItemStyle HorizontalAlign="Center" /> 
                        <ItemTemplate> 
                           <asp:CheckBox runat="server" ID="chkUserInRole"
                              OnCheckedChanged="ChangeUserIsAssignedToRole"
                              AutoPostBack="true" 
                            Checked='<%# IsUserInRole(Container.DataItem) %>' />
                            </ItemTemplate> 
                      </asp:TemplateField> 
                  </Columns> 
             </asp:GridView> 
             <asp:Label runat="server" ID="lblNoUsersFoundRoles"
               visible="false" EnableViewState="false"
               Text="No users found for this search." /> 
             <br /><br /> 
             <asp:LinkButton runat="server" ID="lnkManageAllRoles"
                  Text="Manage All Roles" />&nbsp;&nbsp; 
             <asp:LinkButton runat="server" ID="lnkManageUsersFromRoles"
                  Text="Manage Users" /> 
          </asp:Panel> 
      </ContentTemplate> 
  </asp:UpdatePanel>

You may want to put it towards the top, because putting it to the bottom will cause it to actually display too far toward the bottom. This is due to the fact that a lot of the other sections are styled with display:none. Doing this will leave a section of the rendered page for the sections’ outputs (even if they’re hidden) and push our role management stuff down so it looks weird. Since the other sections are mostly modal dialogs, we can put them at the bottom and they’ll still work as designed.

Before we get to far into it, we’ll also want to add some stuff to our SetUI code block. We’ll be managing a number of panels and UI pieces on various clicks, so let’s add the following to our SetUIModes enumeration:

EditUsersInRole
ManageAllRoles
DeleteRoleConfirm
RenameRoleConfirm
RenameRole

and the following case statements to our SetUI case statement:

Case SetUIModes.EditUsersInRole
    pnlManageRoles.Visible = False
    pnlAddRemoveUsersFromRole.Visible = True
Case SetUIModes.ManageUsers ‘this line may exist already in your code…
    pnlManageRoles.Visible = False
    pnlAddRemoveUsersFromRole.Visible = False
    pnlManageUsers.Visible = True
Case SetUIModes.ManageAllRoles
    pnlManageRoles.Visible = True
    pnlManageUsers.Visible = False
    pnlAddRemoveUsersFromRole.Visible = False
Case SetUIModes.DeleteRoleConfirm
    mpeDeleteRoleConfirm.Show()
Case SetUIModes.RenameRoleConfirm
    mpeRoleRenameConfirm.Show()
Case SetUIModes.RenameRole
    mpeChangeRoleName.Show()

You may have noticed that we previously (in part 3) created a case statement for ManageUsers, we just never populated the code for the case. We can now add this functionality, so just add the code for that case as follows:

pnlManageRoles.Visible = False
pnlAddRemoveUsersFromRole.Visible = False
pnlManageUsers.Visible = True

Now we can get down to coding the pieces we need to make it all functional. First we’ll want to enable the ‘Manage Roles’ link so that we can access our manage role information. This is pretty easy, add the following to your lnkManageRoles_Click event handler:

SetUI(SetUIModes.ManageAllRoles)
SetDataSource(Roles.GetAllRoles, ROLES_DATA_SOURCE)
BindGrid(gvRoles, ROLES_DATA_SOURCE, True)

gvRoles.DataSource = Roles.GetAllRoles()
gvRoles.DataBind()

This will hide our user GridView, and display our Roles GridView. In addition, it will store our datasource and then populate our Roles GridView with all the roles in the database.

Continuing with the easy stuff, once we switch over to the Manage Roles side, we’ll have a link that takes us back to managing users, we should also hook that up. Add the following to your lnkManageUsers_Click event handler:

SetUI(SetUIModes.ManageUsers)

Really all we’re doing is hiding one and showing the other, not much to it.

Next, let’s hook up our Add Role functionality. Add the following to your btnAddRole_Click event handler:

Try
    Roles.CreateRole(txtNewRoleName.Text)
    txtNewRoleName.Text = String.Empty
    gvRoles.DataSource = Roles.GetAllRoles()
    gvRoles.DataBind()
Catch ex As Exception
    lblMessageDialog.Text = ex.Message
    SetUI(SetUIModes.MessageDialog)
End Try

This is a little more complex, but still pretty simple. We put our attempt to add the role within a try catch block so that if something goes wrong: A. it doesn’t crash, and B. we can let the administrator know what the matter is. We’ll first attempt to create the role, if we get an error, this is where it will happen. If adding the role is successful, then we’ll clear the textbox, and then rebind our GridView with fresh data. If adding the role fails, we’ll display our message dialog box, and show the administrator the error that was returned in the exception.

Next, we’ll concentrate on the links in each row of the GridView. Let’s start with the ‘Delete Role’ link. If you notice in our front-end code, each or our links in the row of our GridView calls ‘RoleLinkButtonClick’ when they are clicked. This means that we’ll need to create a RoleLinkButtonClick event handler in our code. We’ll need to include information typical to an event handler as far as parameters are concerned. Since all the links call the same event handler, we’ll need to  determine which was selected, we’ll use a if then statements for that. Create your event handler as follows:

Public Sub RoleLinkButtonClick(ByVal sender As Object, _
    ByVal e As CommandEventArgs) 
   
   ‘If e.CommandArgument.Equals("DeleteRole") Then
   If e.CommandName.Equals("DeleteRole") Then

   End If
End Sub

We’ve just created a shell for our event handler and for our delete role section of it. Before we actually delete anything we’ll want to confirm with the administrator that they want to delete the role. We’ll need to create a confirmation dialog in our front-end so that we can display it. Add the following to your front-end code. It is a dialog, so it can go towards the bottom if you so desire:

<%-- Delete Role Confirmation Dialog --%>
<asp:UpdatePanel ID="upnlDeleteRoleConfirmation" runat="server"
       ChildrenAsTriggers="true">
   <ContentTemplate>
      <asp:Panel ID="pnlDeleteRoleConfirmation" runat="server"
               CssClass="modalPopup" style="display:none; text-align:center;">
         <asp:Literal runat="server" ID="lblDeleteRoleConfirmMessage"
            Text="" />
         <asp:Literal runat="server" ID="lblRoleToDelete" Visible=”False” />
         <br><br />
         <asp:Button runat="server" ID="btnDeleteRoleYes" Text="Yes" />
            &nbsp; 
         <asp:Button runat="server" ID="btnDeleteRoleNo" Text="No" /> 
      </asp:Panel>
      <asp:Button runat="server"
         ID="btnFakeShowMPEDeleteRoleConfirmButton"
         style="display: none;" />
      <cc1:ModalPopupExtender ID="mpeDeleteRoleConfirm" runat="server"
         TargetControlID="btnFakeShowMPEDeleteRoleConfirmButton"
         PopupControlID="pnlDeleteRoleConfirmation"
         BackgroundCssClass="modalBackground" />
   </ContentTemplate>
</asp:UpdatePanel>

We create a dialog, not much of note, a yes and a no button. The one exception is a hidden literal control that we can use to later pull the associated username. With the dialog complete, we can add the innards to our event handler. Add the following to the If statement for ‘DeleteRole’:

lblRoleToDelete.Text = CStr(e.CommandArgument)
lblDeleteRoleConfirmMessage.Text = "Are you sure you want to delete role: "
   & lblRoleToDelete.Text
SetUI(SetUIModes.DeleteRoleConfirm)

This just loads up our dialog, and then show it. Now we need to hookup our ‘Yes’ and ‘No’ buttons. First, let’s hookup our ‘No’ button, add the following to our btnDeleteRoleNo_Click event handler:

mpeDeleteRoleConfirm.Hide()

Basically, we just hide the dialog when the user clicks ‘No’. Now, for our ‘Yes’ button, add the following to our btnDeleteRoleYes_Click event handler:

Try
    Roles.DeleteRole(lblRoleToDelete.Text)
    gvRoles.DataSource = Roles.GetAllRoles()
    gvRoles.DataBind()
Catch ex As Exception
    lblMessageDialog.Text = ex.Message()
    SetUI(SetUIModes.MessageDialog)
End Try

Again, we’ve got a try catch block in case something terrible happens. We’ll attempt to delete the role, if it fails, we’ll jump to the catch portion of the bock and display the failure message. If it is successful, we’ll refresh the GridView and display the new data.

Next, well hookup the functionality for assigning users to the role. First, we’ll want to setup a constant for our Users_In_Role datasource add the following constant to your back-end code:

Private Const USERS_IN_ROLE_DATA_SOURCE As String = _ 
    "USERS_IN_ROLE_DATA_SOURCE"

Then, we’ll setup a helper function to handle binding the users in role grid and accounting for an empty dataset. Add the following helper subroutine to your code:

Public Sub BindUsersForRoleGrid() 
  gvUsersForRole.DataSource = theSession(ROLES_DATA_SOURCE
  gvUsersForRole.DataSource = _
        theSession(USERS_IN_ROLE_DATA_SOURCE)

  gvUsersForRole.DataBind() 
  If gvUsersForRole.Rows.Count = 0 Then 
      lblNoUsersFoundRoles.Visible = True 
  End If
End Sub

We’re creating a subroutine that will populate our GridView’s data source and then examine the GridView. If it is empty, then we’ll show the message about no users being assigned to to the role.

Now we’re ready to add the actual functionality for when we click the ‘Assign Users’ link in the role’s row. Add the following to your RoleLinkButtonClick event handler, after the End If for DeleteRole:

If e.CommandName.Equals("EditUsersInRole") Then
    lblRoleToAddRemoveUsersTo.Text = CStr(e.CommandArgument)
    PopulateAlphabetRepeater(rptAlphabetRepeaterRoles)
    SetDataSource(Roles.GetUsersInRole _ 
             (lblRoleToAddRemoveUsersTo.Text), ROLES_DATA_SOURCE)
    SetDataSource(Roles.GetUsersInRole _
        (lblRoleToAddRemoveUsersTo.Text), _
         USERS_IN_ROLE_DATA_SOURCE)
    BindUsersForRoleGrid() 
    SetUI(SetUIModes.EditUsersInRole)
End If

With that added, we should be able to click our ‘Assign Users’ link and have our ‘Users in Role’ GridView displayed (if there are users in the role). You’ll notice though, that there are a couple of links at the bottom to allow us to navigate back to other editing functions, but they don’t currently work. Let’s hook them up so we can return. Add the following to our lnkManageAllRoles_Click event handler:

SetUI(SetUIModes.ManageAllRoles)

and add the following to our lnkManageUsersFromRoles_Click event handler:

SetUI(SetUIModes.ManageUsers)

Now we should be able to return to previous screens without problem. If you run our application, select to manage roles and then select to assign users to a role, only the users that are currently assigned to our role will be displayed in the GridView. To see other users and assign them to the role, we’ll need to hookup the functionality for the alphabet repeater and search portion at the top. First, let’s create a helper function. This function will take a MembershipUserCollection and convert it to a string array containing usernames. We’ll do this because when we pull Roles.GetUsersInRole, we’ll get a string array, but when we pull from the Membership.FindUser functions, we’ll get a MembershipUserCollection back. Since we’re binding both lists to the same GridView (depending on how we search), it’s a lot easier if they both go to our BindGrid as the same data types. So, add the helper function as follows:

Private Function ConvertMUCollToStringArray( _
  ByVal theMembershipCollection As MembershipUserCollection) As String()

    Dim usersInRole As New List(Of String)
    For Each user As MembershipUser In theMembershipCollection
        usersInRole.Add(user.UserName)
    Next
    Return usersInRole.ToArray()
End Function

This will iterate through the collection, and put the users in a format that we can return as an array. Next, we’ll add functionality for clicking on a letter. Add the following to the rptAlphabetRepeaterRoles_ItemCommand event handler:

gvUsersForRole.PageIndex = 0
Dim arg As String = e.CommandArgument.ToString()

Dim users As MembershipUserCollection = Nothing
If arg.ToLower() = "all" Then
    users = Membership.GetAllUsers()
Else
    users = Membership.FindUsersByName(arg + "%")
End If
SetDataSource(ConvertMUCollToStringArray(users), _ 
  
ROLES_DATA_SOURCE)
BindGrid(gvUsersForRole, ROLES_DATA_SOURCE, True)

SetDataSource(ConvertMUCollToStringArray(users), _
     USERS_IN_ROLE_DATA_SOURCE)
BindGrid(gvUsersForRole, USERS_IN_ROLE_DATA_SOURCE, True)

If gvUsersForRole.Rows.Count = 0 Then
    lblNoUsersFoundRoles.Visible = True
End If

First, we’ll reset the PageIndex on our Gridview, and the pull the argument that was passed in (the letter that was clicked). Next, we’ll create an object to hold the the users found when searching for our argument, and then pull the appropriate subset of users. Next, we set our datasource (notice that in the process we convert our users object through our helper function to a string array), and then bind our GridView. Finally, we’ll check to see if we have an empty dataset and if so, display a message that no users were found.

Finally, in relation to our search portion, we’ll hookup the search button for the text entry portion. Add the following to our btnSearchUsersForRoles_Click event handler:

Dim users As MembershipUserCollection = _
    SearchForUsers(ddlSearchForUsersRole, txtSearchUsersForRoles)
SetDataSource(ConvertMUCollToStringArray(users), ROLES_DATA_SOURCE)
BindGrid(gvUsersForRole, ROLES_DATA_SOURCE, True)
If gvUsersForRole.Rows.Count = 0 Then
    lblNoUsersFoundRoles.Visible = True
End If

When the user clicks the search button, we’ll retrieve a collection of MembershipUser objects, meeting our criteria. Then we’ll set our datasource, once again converting to string array, and then bind our GridView with the results. Finally, we’ll check for empty dataset, and display a message if no users are found.

Next, let’s add functionality for the ‘User is in Role’ checkbox. When we check or uncheck it, it will update the database adding or removing the user from the role. In our front-end code, we specify that the checkbox’s onCheckChange event calls the ‘ChangeUserIsAssignedToRole’ subroutine. Define it as follows:

Public Sub ChangeUserIsAssignedToRole(ByVal sender As Object, _
        ByVal e As EventArgs)
    Dim theCheckbox As CheckBox = CType(sender, CheckBox)
    Dim theItem As GridViewRow = _
           CType(theCheckbox.Parent.Parent, GridViewRow)
    Dim theLabel As Label = _ 
           CType(theItem.FindControl("lblUsernameRole"), Label)
    If theCheckbox.Checked = True Then
        Roles.AddUserToRole(theLabel.Text, _ 
            lblRoleToAddRemoveUsersTo.Text)
    Else
        Roles.RemoveUserFromRole(theLabel.Text, _
            lblRoleToAddRemoveUsersTo.Text)
    End If
End Sub

First, we retrieve checkbox that was clicked (it’s passed in as a parameter), and then use that to retrieve the GridViewRow containing the checkbox (the checkbox’s parent’s parent). From that we’ll get the username. If the checkbox is now checked, we’ll add the user to the role, if not, we’ll remove the user from the role.

That finishes all the functionality for the ‘Assign Users’ link. Next, we’ll concentrate on the ‘Change Role Name’ functionality. First, we’ll need to create some front-end dialogs to support this functionality. We’ll need to create a confirmation dialog, and we’ll need to create a dialog that allows us to input the new name. We also need a dialog to confirm the name change. Add the following to your front-end code anywhere inside our pnlFormatEverything panel:

<%-- Change Role Name Dialog --%>
<asp:UpdatePanel id="upnlChangeRoleName" runat="server" ChildrenAsTriggers="true">
    <ContentTemplate>
        <asp:Panel ID="pnlChangeRoleName" runat="server"
                 style="display: none;" CssClass="modalPopup"
                HorizontalAlign=”center”>
            Enter a new role name.<br /><br />
            <asp:Table runat="server">
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                         Current role name:
                    </asp:TableCell>
                    <asp:TableCell HorizontalAlign="Left">
                        <asp:Label runat="server" ID="lblCurrentRoleName" />
                    </asp:TableCell>
                    <asp:TableCell></asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                         New role name:
                    </asp:TableCell>
                    <asp:TableCell HorizontalAlign="Left">
                        <asp:TextBox ID="txtNewEditRoleName" runat="server" />
                        <cc1:TextBoxWatermarkExtender ID="tweNewRoleName"
                           TargetControlID="txtNewEditRoleName"
                           WatermarkText="Enter new role name"
                           WatermarkCssClass="watermarked" runat="server" />
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:RequiredFieldValidator ID="rfvNewRoleName"
                           runat="server" Display="Dynamic" text="*"
                           ControlToValidate="txtNewEditRoleName"  
                           ValidationGroup="ChangeRoleName"
                           ErrorMessage="You must enter a new role name."  />
                    </asp:TableCell>
                </asp:TableRow>
                <asp:TableRow>
                    <asp:TableCell ColumnSpan="3">
                        <asp:ValidationSummary ID="vsNewRoleName"
                           runat="server" EnableClientScript="true"
                           DisplayMode="List" ShowSummary="true" 
                           ValidationGroup="ChangeRoleName" />
                    </asp:TableCell>
                </asp:TableRow>
            </asp:Table>
            <br />
            <asp:LinkButton ID="lnkRenameRoleSave"
                CausesValidation="true" runat="server" Text="Save" 
                ValidationGroup="ChangeRoleName" />&nbsp;
            <asp:LinkButton ID="lnkRenameRoleCancel" runat="server"
                Text="Cancel" />
        </asp:Panel>
        <asp:Button ID="btnFakeShowMPEChangeRoleName" runat="server"
             style="display: none;" />
        <cc1:ModalPopupExtender id="mpeChangeRoleName" runat="server"
            TargetControlID="btnFakeShowMPEChangeRoleName"
            PopupControlID="pnlChangeRoleName"
            BackgroundCssClass="modalBackground" />
    </ContentTemplate>
</asp:UpdatePanel>

<%-- Role rename confirm dialog --%>
<asp:UpdatePanel ID="upnlChangeRoleNameConfirm" runat="server">
    <ContentTemplate>
        <asp:Panel ID="pnlChangeRoleNameConfirm" runat="server"
              style="display:none;" CssClass="modalPopup"
              HorizontalAlign="Center">
            <asp:Literal ID="lblRoleToRename" runat="server" isible="false" />
            <asp:Literal ID="lblConfirmRoleRename" runat="server" Text="" />
            <br /><br />
            <asp:Button ID="btnRoleRenameYes" runat="server" Text="Yes" />
            <asp:Button ID="btnRoleRenameNo" runat="server" Text="No" />
        </asp:Panel>
        <asp:Button ID="btnFakeShowMPERoleRenameConfirm" runat="server"
             style="display:none;" />
        <cc1:ModalPopupExtender runat="server"
            ID="mpeRoleRenameConfirm"
            TargetControlID="btnFakeShowMPERoleRenameConfirm"
            PopupControlID="pnlChangeRoleNameConfirm"
            BackgroundCssClass="modalBackground" />
    </ContentTemplate>
</asp:UpdatePanel>

Now, we just need to add the functionality. We’ll need to add another block to our RoleLinkButtonClicked event handler. Add the following If Then statement to the subroutine:

If e.CommandName.Equals("EditRoleName") Then
    lblCurrentRoleName.Text = CStr(e.CommandArgument)
    mpeChangeRoleName.Show()
End If

Basically, we’re just displaying our change role name dialog. We do populate a label in the dialog so that the administrator knows what role is being changed. Next, we need to add functionality to our ‘Save’ and ‘Cancel’ buttons. Add the following to our lnkRenameRoleSave_Click event handler:

lblRoleToRename.Text = lblCurrentRoleName.Text

lblConfirmRoleRename.Text = "Warning, if the role name you are changing is used in your code, renaming a role may break your code. Please proceed with caution!</br><br>Are you sure you want to rename '" & lblRoleToRename.Text & "' to '" & txtNewRoleName.Text & "'?"

SetUI(SetUIModes.RenameRoleConfirm)

We store our role name so that we have it available when we click ‘Yes’, and set a long warning to the administrator letting them know that if they rename a role, they could possibly break their code. Renaming roles is another one of those features that is unsupported by Microsoft. In Microsoft’s defense, it really is a questionable practice though. If you have specified anything in your application calling the role by name, when you rename it here, it will no longer trigger the appropriate functionality in your application, thus breaking your code. (USE THIS FEATURE WISELY…) Add the following to our lnkRenameRoleCancel_Click event handler:

txtNewEditRoleName.Text = String.Empty
lblCurrentRoleName.Text = String.Empty

This just clears out anything hanging around when we cancel the dialog.

Finally, we’ll want to hook up our ‘Yes’ and ‘No’ buttons on our confirmation dialog. We’ll start with ‘No’. Add the following to our btnRoleRenameNo_Click event handler:

SetUI(SetUIModes.RenameRole)

We will just go back to the change role dialog if the administrator doesn’t confirm. Next, add the following to our btnRoleRenameYes_Click event handler:

Dim usersInRole As String() = Roles.GetUsersInRole(lblRoleToRename.Text)
Roles.CreateRole(txtNewEditRoleName.Text)
If usersInRole.Length > 0 Then
    Roles.AddUsersToRole(usersInRole, txtNewEditRoleName.Text) 
    Roles.RemoveUsersFromRole(usersInRole, lblRoleToRename.Text)
End If
Roles.DeleteRole(lblRoleToRename.Text)
txtNewEditRoleName.Text = String.Empty
lblRoleToRename.Text = String.Empty
SetUI(SetUIModes.ManageAllRoles)
gvRoles.DataSource = Roles.GetAllRoles()
gvRoles.DataBind()

You’ll notice, that we can’t just rename the role, the functionality isn’t included in the Microsoft Role provider, so we have to work around it. What we’ll do, is retrieve the users in the role that we want to rename, we’ll check that string array that we retrieve, and if it has any elements, we know that there are users in the role. We’ll add these users to our new role (after we create it) and then remove those users from the old role, and then delete the old role (since we can’t delete a role with users in it). (That’s our work around, add all the users from our old role to the new role, remove them from the old role, and then delete the old role). Once we are done, we’ll clear our dialog and then close it. Finally, we’ll rebind our GridView so that it displays our new role name.

**Author’s Note** – I found one more small problem, paging for the Role’s grids don’t work. Unfortunately in my development, I didn’t put more than one page worth of anything into my database (users or roles). Thus, I never checked to make sure my paging worked, I never needed to page. As a result, paging for both gvRoles and gvUsersForRole will return a JavaScript error. This is because we don’t handle their PageIndexChanging event handler. Since we’re using AJAX, the JavaScript will call the back-end, but cause an error because the event handler doesn’t exist. So to remedy this problem we’ll add the following lines to our gvRoles_PageIndexChanging event handler:

gvRoles.PageIndex = e.NewPageIndex
BindGrid(gvRoles, ROLES_DATA_SOURCE, False)

and the following to our gvUsersForRole_PageIndexChanging event handler:

UsersForRole.PageIndex = e.NewPageIndex
BindGrid(gvUsersForRole, ROLES_DATA_SOURCE, False)
BindGrid(gvUsersForRole, USERS_IN_ROLE_DATA_SOURCE, False)

You’ll also notice that I made a little change in our lnkManageRoles_click event handler. To be able to use paging, we need to store our datasource, so rather than binding our gvRoles directly from our Roles provider (Roles.GetAllRoles()), we’ll set our DataSource and then use it later when we call our PageIndexChanging event handler.

Now paging works. Funny what you find when you do some real testing…

Epilogue

And there we have it. The final pieces of our functionality. All we need to manage all aspects of our Roles. We can now Add, Delete, Rename, and assign/unassign users from our roles.

Technically, we’re done with our control, we’ve done everything we need to do to successfully use it, however, I think that I’ll do one more post in this series offering some optional enhancements that I may someday use if I feel like it, but that may come in useful none the less.

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

Prologue

**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**

In previous posts we started creating “A Better ASP.Net Member/Role Management Page.” In part 1, we set criteria, and in part 2, part 3 and part 4 we created functionality to add, delete and edit users. We’ve finished nearly all the functionality for editing users with one exception, changing usernames. We’ll be addressing that in this post.

Problem

Changing a username is functionality that isn’t included in the default Microsoft membership provider. The reason for this, in my understanding, is that changing usernames is considered somewhat of a security issue. Really,i don’t understand why it isn’t included, changing a username could be put together in such a way that it is reasonably secure, you know require passwords and or security questions etc. like practically everything else that you try to change using the membership provider does. Although the functionality isn’t included in the Microsoft provider, we can add the functionality on our own (and it’s actually relatively easy).

Solution

Unfortunately, this functionality will be the one part that we won’t be able to contain fully in our page, we’ll have to add a stored procedure to our database. Well, I guess we could put all our SQL stuff in a subroutine, but that’s leaves us with a security problem, accepting user in put and just inserting into a SQL statement and running it directly form our page leaves us open for SQL injection attack. The most secure thing would be to create a stored procedure in the database, and like I said that means that we don’t have a fully self contained page.

To workaround this limitation, what we’ll do is add a property to the page so that we can indicate that we have created the stored procedure and we can change the username. This will turn on/off the ‘change username’ link on our page.

Let’s start with the SQL stored procedure. If you’ve ever looked at a database that’s enabled for the ASP.net membership provider you will notice that enabling a database for ASP.Net membership provider adds a number of stored procedures. We’ll add another stored procedure and we’ll use the same naming convention. To add the stored procedure execute the following SQL code:

CREATE PROCEDURE aspnet_Membership_ChangeUsername
    @currentUsername nvarchar(255),
    @newUsername nvarchar(255),
    @results bit OUTPUT
AS
    UPDATE aspnet_Users
    SET UserName = @newUsername,
        LoweredUserName = LOWER(@newUsername)
    WHERE LOWER(UserName) = LOWER(@currentUsername)
    set @results = @@ROWCOUNT

The stored procedure here will take three parameters, one for each of the usernames, the new and the old, and one parameter to output the success of the operation. It will return 0 on failure and 1 on success (as one row was changed on success). The actual username data is actually only stored in one location, in the user table. Rather than using the username as the ID to link with all other tables, there’s a unique UserID in the user table that is used. Changing the username is easy, we have two places to change it in one table and it doesn’t take much.

Once that’s done, we need to add the functionality to our page. We don’t have much to add to the front-end code, just the change username dialog and a new style. Add the following to your style section:

.taken
{
    color: Red;
}

and the following to your front end code. Place it at the end of the page, but before our closing tag for our pnlFormatEverything panel:

<%-- Change Username Dialog --%>
<asp:UpdatePanel id="upnlChangeUsername" runat="server" ChildrenAsTriggers="true">
    <ContentTemplate>
        <asp:Panel ID="pnlChangeUsername" runat="server"
                style="display: none;
                text-align:center;" CssClass="modalPopup">
            <b>Enter a New Username</b><br /><br />
            <asp:Table runat="server" ID="tblChangeUsername">
                <asp:TableRow>
                    <asp:TableCell HorizontalAlign="Right">
                       Current Username:
                    </asp:TableCell>
                    <asp:TableCell HorizontalAlign="Left">
                        <asp:Label ID="lblCurrentUsername"
                             runat="server" Text="" />
                    </asp:TableCell>
                    <asp:TableCell></asp:TableCell>
                </asp:TableRow>
                <asp:Tablerow>
                    <asp:TableCell HorizontalAlign="Right">
                       New Username:
                    </asp:TableCell>
                    <asp:TableCell HorizontalAlign="Left">
                        <asp:TextBox ID="txtNewUsername" runat="server" />
                        <div runat="server" id="divUserAvailablity"
                             class="taken"></div>

                        <cc1:TextBoxWatermarkExtender
                        ID="tbeNewUsername" runat="server"
                        TargetControlID="txtNewUsername"
                        WatermarkText="Enter New Username"
                        WatermarkCssClass="watermarked">
                        </cc1:TextBoxWatermarkExtender>
                    </asp:TableCell>
                    <asp:TableCell>
                        <asp:RequiredFieldValidator runat="server"
                                ID="rfvNewUsername"
                                ControlToValidate="txtNewUsername"
                                Display="Dynamic"
                                ValidationGroup="ChangeUsername"
                                ErrorMessage="You must enter a new username"
                                Text="*" />
                    </asp:TableCell>
                </asp:Tablerow>
                <asp:TableRow>
                    <asp:TableCell ColumnSpan="3">
                        <asp:ValidationSummary ID="vsNewUsername"
                               ValidationGroup="ChangeUsername" runat="server"
                               DisplayMode="List" ShowSummary="true"
                               EnableClientScript="true" />
                    </asp:TableCell>
                </asp:TableRow>
            </asp:Table>
            <asp:LinkButton ID="lnkChangeUsernameSave"
                  runat="server" Text="Save"
                  CausesValidation="true" ValidationGroup="ChangeUsername" />
                  &nbsp;&nbsp;&nbsp;
            <asp:LinkButton ID="lnkChangeUsernameCancel"
                 runat="server" Text="Cancel" />
        </asp:Panel>
        <asp:Button ID="btnFakeShoeMPEChangeUsernameDialogButton"
               runat="server" style="display:none;" />
        <cc1:ModalPopupExtender ID="mpeChangeUsername" runat="server"
        TargetControlID="btnFakeShoeMPEChangeUsernameDialogButton"
        PopupControlID="pnlChangeUsername"
        BackgroundCssClass="modalBackground">
        </cc1:ModalPopupExtender>
    </ContentTemplate>
</asp:UpdatePanel>

Basically we’re just creating a label to put in the existing username, a textbox for the new username and then save and cancel buttons. We also add a div so that we can tell the user if the new username is already being used.

Now let’s add the back end code. First, we’ll add another entry to our SetUI enumeration, and another case to our SetUI case statement. Add the following line to our enumeration:

ChangeUsername

and add the following case statement to our SetUI Case statement:

Case SetUIModes.ChangeUsername
   mpeChangeUsername.Show()

Next, we’ll hook up the ‘Change Username’ LinkButton. It’s functionality is pretty easy. Add the following to the our lnkChangeUsername_Click event handler:

lblCurrentUsername.Text = lblUsernameEdit.Text
SetUI(SetUIModes.ChangeUsername)

We set the current username and then show the dialog.

We don’t need to do anything with the cancel button as it will function correctly without help, however we need to hook up the functionality for the Save button. We’ll want to do a little checking to see if the username is being used before we actually do any changing. We’ll want to display a message that it’s taken if it is. We’ll also want to make sure that the username was successfully changed in the database when we actually execute the SQL. Well add two helper functions, the first to check username availability. Add the following function:

Private Function CheckUsernameIsAvailable( _
          ByVal sUsernameToCheck As String) As Boolean

    If IsNothing(Membership.GetUser(sUsernameToCheck)) = False Then
        Return False
    Else
        Return True
    End If
End Function

Basically, we receive the new username and then check to see if we can retrieve a MembershipUser object from the database. If we can, then the username is NOT available and we return false.

We’ll also want to create a helper function to perform the actual database work, since our membership provider doesn’t do this for us. Add the following helper function (I apologize in advance for dividing of the lines in such a terrible manner…):

Private Function ChangeUsername(ByVal sCurrentUsername As String, _
         ByVal sNewUsername As String, _
         Optional ByVal iOrdinalForConnectionString As Integer = 0) As Boolean 

   Using cn As New SqlConnection( _
      ConfigurationManager.ConnectionStrings( _
        iOrdinalForConnectionString).ConnectionString) 
        
      Dim cmd As New SqlCommand _
             ("aspnet_Membership_ChangeUsername", cn) 
      cmd.CommandType = Data.CommandType.StoredProcedure 
      cmd.Parameters.Add _
        ("@currentUsername", Data.SqlDbType.NVarChar).Value _
         = sCurrentUsername 
      cmd.Parameters.Add _
        ("@newUsername", Data.SqlDbType.NVarChar).Value _
           = sNewUsername 
      cmd.Parameters.Add("@Results", Data.SqlDbType.Bit).Direction _
          = Data.ParameterDirection.Output  
      cn.Open()
      Try
         cn.Open() 
         cmd.ExecuteNonQuery() 
      Catch ex As Exception 
         Return False 
      Finally 
         cn.Close() 
      End Try 
      Return cmd.Parameters("@Results").Value 
   End Using
End Function

This function is easily the least flexible in the entire object. Notice we pass in both the old and the new usernames. We also pass in an ordinal for the connectionString to use. Unfortunately we are unable to determine the connectionString currently being used in the membership provider, so we have to pass in the ordinal of the connection string we are using. In my case, I don’t even have a connectionString as I’m using the ASP.Net default sqlexpress database, so an ordinal of 0 works for me. You may need to play with what you pass in depending on your connectionstring setup. We use this connectionString to create a connection. With this connection, we create a command object, load it up with our parameters, including an output parameter. We then attempt to execute the stored procedure. If it fails we return false, otherwise we return the success/failure results from the actual database execution. You’ll notice that we use some SQL Data client objects so we’ll need to import the following namespace (or fully qualify all your SQL objects). Add the following import statement to the top of your document:

Imports System.Data.SqlClient

With our helper functions out of the way, we can now concentrate on our save routine. Add the following to your lnkChangeUsernameSave_Click event handler:

If CheckUsernameIsAvailable(txtNewUsername.Text) = False Then
    divUserAvailablity.InnerText = "Username taken, sorry."
    SetUI(SetUIModes.ChangeUsername)
    Exit Sub
End If
If ChangeUsername(lblCurrentUsername.Text, txtNewUsername.Text) = True Then
    SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
    BindGrid(gvManageUsers, USERS_DATA_SOURCE, False)
    lblMessageDialog.Text = "Username successfully changed."
Else
    lblMessageDialog.Text = "Username change failed."
End If
txtNewUsername.Text = String.Empty
divUserAvailablity.InnerText = String.Empty
SetUI(SetUIModes.MessageDialog)

First we check to make sure the username is available. If it isn’t, then we’ll put up a message much like our validation letting the administrator know that it’s taken. We’ll make sure our change username dialog is still up and then exit the sub. If it’s valid, we attempt to perform the actual change. If the change is successful, we’ll refresh our data source, and then update our GridView with the new username information. Whether it fails or is successful, we’ll set the message dialog, and then when all is said and done, show the message. We’ll also reset our new username TextBox and our availability div so that they are both blank and ready to use next time.

Ok, so we’ve got all our functionality working, but what if a user of our control didn’t put the stored procedure into the database? We’ll need to disable the functionality in that case. Actually, what we’ll do is disable the functionality be default, and then add it back in if the control’s user specifically enables it. We’ll add a property to our user control that will control the ‘Change username’ link so that the functionality to call the DB is only available if the control’s user specifically configures it to say that the DB has been setup. Add the following to define our property:

Public Enum ChangeUsernameSettings
    DbIsSetup
    DbIsNotSetup
    DisableLink
End Enum

Private _ChangeUsernameFunctionality As ChangeUsernameSettings _
                = ChangeUsernameSettings.DbIsNotSetup

Public Property ChangeUsernameFunctionality()
    Get
        Return _ChangeUsernameFunctionality
    End Get
    Set(ByVal value)
        _ChangeUsernameFunctionality = value
    End Set
End Property

Here, we create a enumeration for our different settings. Next we create a private variable to hold our property’s current setting, and set it by default to be DbIsNotSetup. Finally we create our property.

Now we’ll need to implement some things so that the settings are used. Add the following line to your SetupEditUser subroutine (anywhere will work):

If _ChangeUsernameFunctionality = ChangeUsernameSettings.DisableLink _
       Then lnkChangeUsername.Enabled = False

If disableLink is selected, then we’ll disable it during our EditUser dialog setup. We could add a tooltip if desired, letting the administrator using the page what needs to be done to add the functionality. Next add the following to your lnkChangeUsername_Click event handler (apologies for the big block of text in the middle that has no link breaks (for readability)):

If _ChangeUsernameFunctionality  =  _ 
              ChangeUsernameSettings.DbIsNotSetup Then
    lblMessageDialog.Text = _
"For this fundtion to be available, the following must be met: <br> 1. The database needs to have a stored procedure created for this functionality and <br> 2. the administrator must explicitly control to specify that the functinality is in the database.<br><br> Please see the documentation for more information."

    SetUI(SetUIModes.MessageDialog)
    Exit Sub
End If

If our page creator doesn’t specifically enable on this functionality, the administrator using our control will receive a message letting them know what needs to happen for the functionality to be enabled. And of course, if we set the property to DbIsSetup, then we’ll have our functionality back.

Epilogue

Ugh… as in ugly. Truly, this isn’t the cleanest portion of our functionality, we are completely disregarding just about every one of our rules about keeping this control self contained and trying to make it useful across the board. But, in our defense, we’re making up for functionality limitations built into the the .Net framework (or is it limitations NOT built into…?). We could of course extend, or rewrite our membership provider, but I choose not to do it that way.

This should fulfill all the requirements we had previously for our membership portion of our management page. Now we just need to work on the roles administration portion. That will be the subject of the next post(s).

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

Prologue

**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**

Previously, we started work on “A better ASP.Net Member/Role Management Page”. So far, we looked at requirements in Part 1, we then added functionality in Part 2 to display, filter and search existing user and finally, in Part 3, we added functionality to create and delete users from the system. In this post, we’ll be adding functionality to edit a user’s account information.

Problem

In this post, we’ll be creating the functionality we need to edit users. This is perhaps one of the most convoluted lengthy parts of our series. We’ll run into a number of places where the functionality isn’t as simple as it seems or that we have limits based on how we’ve configured our provider. Once we’re finished with this post, we should have completed all of our requirements to complete #3 on our criteria list, with the exception of renaming (which will be addressing in the next post).

Solution

Let's get started by creating all the front end markup for our Edit User dialog. We’re going to introduce a lot of functionality in this post, and this markup will encapsulate most of it. The front-end code is kind of unwieldy, but here it is. I’ll point out a couple of noteworthy items after the listing. Add the following after your last section, but before the closing tag for our formatEverything panel:

<%-- Edit User Dialog –%>
   <asp:UpdatePanel ID="upnlEditUser" runat="server"
               ChildrenAsTriggers="true">
      <ContentTemplate>
         <asp:Panel ID="pnlEditUser" runat="server" CssClass="modalPopup"
                     style="display:none;">
               <asp:Table runat="server">
                  <asp:TableRow>
                     <asp:TableCell Width="75%">
                        <asp:Table runat="server">
                           <asp:TableRow>
                              <asp:TableCell ColumnSpan="3">
                                 <b>Edit User Details:</b>
                              </asp:TableCell>
                           </asp:TableRow>
                           <asp:TableRow>
                              <asp:TableCell HorizontalAlign="Right">
                                 Username:
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:Label ID="lblUsernameEdit"
                                       runat="server" Text="" />
                              </asp:TableCell> 
                              <asp:TableCell>
                                 <asp:LinkButton ID="lnkChangeUsername"
                                        runat="server" text="Change Username" /> 
                              </asp:TableCell>
                           </asp:TableRow>
                           <asp:TableRow>
                              <asp:TableCell HorizontalAlign="Right">
                                 Email:
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:TextBox ID="txtEmail" runat="server" />
                                 <cc1:TextBoxWatermarkExtender ID="tweEmail"
                                          runat="server" TargetControlID="txtEmail”
                                          WatermarkCssClass="watermarked"
                                          WatermarkText="Enter eMail">
                                 </cc1:TextBoxWatermarkExtender>
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:RegularExpressionValidator ID="revEmail"
                                       runat="server" ErrorMessage="eMail not valid."
                                       ControlToValidate="txtEmail" Display="Dynamic"
                                       ValidationExpression="\S+@\S+\.\S+" />
                              </asp:TableCell>
                           </asp:TableRow>
                           <asp:TableRow>
                              <asp:TableCell HorizontalAlign="Right">
                                 Description:
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:TextBox ID="txtDescription" runat="server" />
                                 <cc1:TextBoxWatermarkExtender ID="tweDescription"
                                       TargetControlID="txtDescription"
                                       WatermarkCssClass="watermarked"
                                       WatermarkText="Enter description"
                                       runat="server">
                                 </cc1:TextBoxWatermarkExtender>
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:LinkButton ID="lnkResetPassword"
                                         runat="server" Text="Reset Password" />
                              </asp:TableCell>
                           </asp:TableRow>
                           <asp:TableRow>
                              <asp:TableCell HorizontalAlign="Right">
                                 Password:
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:Label ID="lblPassword" runat="server"
                                       Text="********" />
                              </asp:TableCell>
                              <asp:TableCell>
                                    <asp:LinkButton ID="lnkChangePassword"
                                          runat="server" Text="Change Password" />
                              </asp:TableCell>
                           </asp:TableRow>
                           <asp:TableRow>
                              <asp:TableCell HorizontalAlign="Right">
                                 Security Question:
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:Label ID="lblSecurityQuestion"
                                       runat="server" Text="" /> 
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:LinkButton ID="lnkChangeSecurityQuestion"
                                       runat="server" Text="Change Security Q/A" />
                              </asp:TableCell>
                           </asp:TableRow>
                           <asp:TableRow>
                              <asp:TableCell HorizontalAlign="Right">
                                 Active User:
                              </asp:TableCell>
                              <asp:TableCell>
                                 <asp:CheckBox ID="chkActiveUser"
                                    runat="server" Text="" Checked="true" />
                              </asp:TableCell>
                              <asp:TableCell></asp:TableCell>
                           </asp:TableRow>
                        </asp:Table>
                     </asp:TableCell>
                     <asp:TableCell>
                     <b>
                        <asp:Label ID="lblEditUserSelectRoles"
                              runat="server" Text=”Roles” />
                     </b>
                     <br />
                     <asp:Label ID="lblEditAddUserToRole"
                           runat="server" Text="" />
                     <br />
                     <asp:Repeater ID="rptEditUserRoles" runat="server">
                        <ItemTemplate>
                           <asp:CheckBox ID="chkRole" runat="server"
                              Text="<%# Container.DataItem.ToString() %>" />
                           <br />
                        </ItemTemplate>
                     </asp:Repeater>
                  </asp:TableCell>
               </asp:TableRow>
               <asp:TableRow>
                  <asp:TableCell ColumnSpan="2" HorizontalAlign="Center">
                     <asp:LinkButton ID="lnkEditUserSave" runat="server"
                           Text="Save" />
                     &nbsp;&nbsp;
                     <asp:LinkButton ID="lnkEditUserCancel" runat="server"
                           Text="Cancel" CausesValidation=”False” />
                  </asp:TableCell>
               </asp:TableRow>
            </asp:Table>
         </asp:Panel>
         <asp:Button ID="btnFakeMPEEditUser" runat="server"
               style="display:none;" />
         <cc1:ModalPopupExtender ID="mpeEditUser" runat="server"
               TargetControlID="btnFakeMPEEditUser"
               PopupControlID="pnlEditUser"
               BackgroundCssClass="modalBackground">
         </cc1:ModalPopupExtender>
   </ContentTemplate>
</asp:UpdatePanel>

I’m going against my anti-table-as-layout leanings again, and doubling up on that breach of protocol by nesting tables. In our code, we create a two row table, in the first row, we add almost everything except the buttons that will save or cancel. In the second row we add the save and cancel buttons. The first row has 2 cells one that holds a table with all the user information, and another with the role information. You’ll notice that we have a couple of WatermarkExtenders for some of the text boxes.

We also have one minor change to our cancel button over other’s we’ve used. You’ll notice that we add CausesValidation=”false”. This will allow our button to close the dialog even if the entries in our form are not valid (otherwise we’d have to get everything valid before we can even cancel).

You’ll also notice that we have a number of items that we’ll link to yet other dialogs that provide the actual functionality (i.e. ‘change password’, ‘change username’ and ‘change security Q/A’). These are a little more tricky to implement. For this reason, the actual functionality for these will not be directly effected by the ‘Edit User’ dialog, rather they will work semi-independently of the dialog (i.e. they will be available from the ‘Edit User’ dialog, but will not function based on clicking save/cancel on the ‘Edit User’ dialog).

With the front-end markup code finished, we’re ready to get started on the back-end coding. Flip over to your code behind and let’s get started. First we’ll want to hookup our ‘Edit User’ link to open our new dialog. If you remember, our Delete User link’s event handler was the GridUsersClick subroutine. We’ll need to add another if statement to our GridUsersClick event handler to catch the ‘Edit User’ click. Add the following to your GridUsersClick subroutine, it doesn’t matter if it is before or after the DeleteUser’s if statement:

If e.CommandName.Equals("EditUser") Then
   SetupEditUser(e.CommandArgument)
   SetUI(SetUIModes.EditUser)
End If

We’ll call a helper function to do a number of little things that it takes to populate our ‘Edit User’ dialog. We are also passing the CommandArgument from the GridUsersClick to the routine, this will pass our username over to the setup routine so we know which user’s information to pull. Create our SetupEditUser subroutine as follows:

Private Sub SetupEditUser(ByVal sUsername As String)
   lblUsernameEdit.Text = sUsername
   Dim theUser As MembershipUser = Membership.GetUser(sUsername)
   If IsNothing(theUser) Then Exit Sub
   txtEmail.Text = theUser.Email
   txtDescription.Text = theUser.Comment
   chkActiveUser.Checked = theUser.IsApproved
   lblSecurityQuestion.Text = theUser.PasswordQuestion
   SetDataSource(Roles.GetAllRoles, ROLES_DATA_SOURCE)
   BindRepeater(rptEditUserRoles, ROLES_DATA_SOURCE)
   SetupEditUserRoles(sUsername) 
End Sub

First we grab the username and use it to retrieve the membership information for the user. The object that is returned has all the user’s accessible information. If a valid object is returned, then we’ll process the remainder of the routine. We pull each piece of information and display it. Then we bind the repeater for Roles with all the roles, and call another helper function SetupEditUserRoles. We’ll use this to iterate through our repeater items and mark the roles that our user is currently assigned to. Finally we SetUI so that our ‘Edit User’ dialog will appear. To make this helper routine function we need to add a couple things. First, let’s add an entry to the SetUIModes Enumeration for ‘EditUser’ and add the following case to our SetUI subroutine:

Case SetUIModes.EditUser
   mpeEditUser.Show()

We also need to create our SetupEditUserRoles helper function so let’s define it as follows:

Private Sub SetupEditUserRoles(ByVal sUserName As String)
   If rptEditUserRoles.Items.Count > 0 Then
      lblEditAddUserToRole.Text = "Select Roles for user:"
      For Each theItem As RepeaterItem In rptEditUserRoles.Items
         Dim chkRole As CheckBox = _
               CType(theItem.FindControl("chkRole"), CheckBox)
         chkRole.Checked = Roles.IsUserInRole(sUserName, chkRole.Text)
      Next
   Else
      lblEditAddUserToRole.Text = "No roles defined."
   End If
End Sub

If our repeater has any items, then we’ll add administrator directions to our ‘Edit User’ dialog, and then iterate through each of the items in the repeater. We pull out the checkbox from the repeater’s item and if see if the user is in the role associated with the checkbox. If they are, then we’ll mark it checked. If there are not any roles, we’ll also notify the administrator of such.

Running your application at this point should allow you to select ‘Edit User’ and get all the details about our user. If we have roles, they’ll show up and if we have any assigned to the user, they’ll be indicated as such. Now we need to hookup our ‘Save’ button so that our changes are saved. (‘Cancel’ already automatically closes the dialog without saving). Add the following within our lnkEditUserSave_Click event handler:

If Not Page.IsValid() Then Return
Dim sUserName As String = lblUsernameEdit.Text
Try
   Dim theEditedUser As MembershipUser = _
      Membership.GetUser(sUserName)
   theEditedUser.Email = txtEmail.Text
   theEditedUser.Comment = txtDescription.Text
   theEditedUser.IsApproved = chkActiveUser.Checked
   Membership.UpdateUser(theEditedUser)
   UpdateRoleMembership(rptEditUserRoles, sUserName)
Catch ex As Exception
   lblMessageDialog.Text = "Error updating user and/or roles."
   SetUI(SetUIModes.MessageDialog)
   Return
End Try

First we check if the page is valid (our validation controls will set this). If it isn’t we’ll exit, if it is we’ll retrieve the username and use that to retrieve their MembershipUser object. From there we’ll update the pieces of the User’s information and then save the changes to the database. Finally, we update the role membership. We already created the helper subroutine to assign roles, so it should perform without any trouble. We wrap this in a try catch block so that if it fails we can notify the administrator that update failed. You may also notice that if update fails, we reuse the dialog that we created earlier to notify the administrator.

Now that we’ve saved the user’s new information we need to update our GridView’s datasource (and thus the GridView). We’ll also notify our administrator that the changes were successful. Add the following to our lnkEditUserSave_Click event handler. Below our End Try statement:

SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
BindGrid(gvManageUsers, USERS_DATA_SOURCE, True)
lblMessageDialog.Text = "You have successfully updated the user <b>" _
   & sUserName & "</b>."
SetUI(SetUIModes.MessageDialog)
ResetEditUserDialog()

Here we reset our data source and then rebind our GridView. We set our message dialog and then show it. Finally we call a new helper subroutine, ResetEditUserDialog. This will clear out all the information that we put into our ‘Edit User’ dialog. We can also call this when we click the cancel button if desired. This just makes sure that we don’t have any stray information hanging around. Create the new helper subroutine as follows:

Private Sub ResetEditUserDialog()
   lblUsernameEdit.Text = String.Empty
   txtDescription.Text = String.Empty
   txtEmail.Text = String.Empty
   lblSecurityQuestion.Text = String.Empty
End Sub

Really all we are doing is flushing out all the data in our dialog box. Now if you run your application and edit a user’s information, we’ll be able to successfully change and update the user’s information.

You may have noticed, but we still have a checkbox in the main GridView that we haven’t wired up with anything but a stub. (the checkbox in front of the user’s name). We can wire that up pretty easily. If you remember back, to the gridUsersClick portion, we’ll need to define some parameters to receive information from the actual event (since currently our stub doesn’t have them). From there we’ll be able to work our magic. Modify the ChangeUserIsApproved stub as follows:

Public Sub ChangeUserIsApproved(ByVal sender As Object, _
       ByVal e As EventArgs)

With these parameters, we can then access the properties that we need to change the user’s approved status. Add the following as the body of your subroutine and we’ll be good to go there:

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 sUserName As String = theUserNameLabel.Text
Dim theUser As MembershipUser = _ 
       Membership.GetUser(sUserName)
theUser.IsApproved = theCheckbox.Checked
Membership.UpdateUser(theUser)

First we get the checkbox. Then we retrieve the GridViewRow containing the checkbox so that we can get the user’s name. Finally we pull the label with the username from the GridViewRow and use that to retrieve the user from the Membership database. We change the user’s IsApproved status and then the update the user.

That leaves us with 4 LinkButtons on this page that we haven’t yet addressed, ‘Change Username’, ‘Reset Password’, ‘Change Password’ and ‘Change Security Q/A’. I saved these till last because your ability as an administrator to change/reset a user’s password and security Q/A depends on how the Membership provider is configured.

If you aren’t familiar with the setup of membership providers in ASP.Net, I found an excellent article that covers the provider settings and how they affect what you are allowed to do with passwords. Basically if we have set our provider up as the default (HASHED), then we don’t have the option as administrators to change the user’s password, we can only reset it. Also, even if we set our password as encrypted, then we’ll not be able to change the user’s password if our provider sets requiresQuestionAndAnswer to true, because we’d have to have the user’s security answer.

The point is this, if we want to have the ability as an administrator to change the user’s password, we’ll either need to have the user’s password and/or security question or configure our provider in such a way that we can retrieve the password. Here’s the rub, we may not want to configure our provider that way for the users, however it may be appropriate for the administrator. One suggestion I found in my searching revolves around having 2 Membership providers and then using one for the users and one for the administrator. The two providers would both go to the same Membership database, but the difference would be in how you setup your provider details (such as enablePasswordRetrieval etc).

In our case, what we’ll do is examine the provider being used and determine if it meets the requirements necessary to allow us to perform the functions we’d like, if not then we’ll disable the feature. You may decide to try changing your membership provider around a little to test it in each of the scenarios. We’ll use the flowchart on the right to determine what options we present to the administrator.

We’ll want to enable or disable the links on our ‘Edit User’ dialog before it is shown, based on the provider’s settings. While we could do this in the page_load, we’ll create a subroutine that has all our functionality in it and call that subroutine from our editUserClick subroutine. Define our helper subroutine as follows:

Private Const FUNCTIONALITY_NOT_ENABLED As String = _
       "Provider not configured to allow this function."

Public Sub SetupEditPasswordLinks()
   Dim bResetEnabled As Boolean = Membership.Provider.EnablePasswordReset
   Dim bRetrievalEnabled As Boolean = Membership.Provider.EnablePasswordRetrieval
   Dim bQnARequired As Boolean = Membership.Provider.RequiresQuestionAndAnswer

   If Membership.Provider.PasswordFormat = MembershipPasswordFormat.Hashed Then
      lnkChangePassword.Enabled = False
      lnkChangePassword.ToolTip = FUNCTIONALITY_NOT_ENABLED
      lnkChangeSecurityQuestion.Enabled = False
      lnkChangeSecurityQuestion.ToolTip = FUNCTIONALITY_NOT_ENABLED
      If bResetEnabled = False OrElse _
             (bResetEnabled = True AndAlso bQnARequired = True) Then
         lnkResetPassword.Enabled = False
         lnkResetPassword.ToolTip = FUNCTIONALITY_NOT_ENABLED
      End If
    Else
       If bRetrievalEnabled = False OrElse _
                (bRetrievalEnabled = True AndAlso bQnARequired = True) Then
          lnkChangePassword.Enabled = False
          lnkChangePassword.ToolTip = FUNCTIONALITY_NOT_ENABLED
       End If
       If bResetEnabled = False OrElse _
              (bResetEnabled = True AndAlso bQnARequired = True) Then
          lnkResetPassword.Enabled = False
          lnkResetPassword.ToolTip = FUNCTIONALITY_NOT_ENABLED
       End If
       If bRetrievalEnabled = False Then
          lnkChangeSecurityQuestion.Enabled = False
          lnkChangeSecurityQuestion.ToolTip = FUNCTIONALITY_NOT_ENABLED
       End If
   End If
   If bRetrievalEnabled = False Then
      lnkChangeSecurityQuestion.Enabled = False
      lnkChangeSecurityQuestion.ToolTip = FUNCTIONALITY_NOT_ENABLED
   End If
End Sub

In this subroutine, we run through the scenarios in our flowchart and enable/disable our links for changing things dependant on our provider’s settings. We also add an if statement to enable/disable our Change Security Q/A link if password retrieval is not enabled (as the password needs to be passed to the method to perform the change). Now we just need to call our helper subroutine. Add the following line to our gridUsersClick subroutine, just after the SetupEditUser call:

SetupEditPasswordLinks()

You should now have your links enabled or disabled as appropriate to your provider configuration. Now we just need to make all the links functional. First, we’ll need to create dialog boxes that allow us to enter the information for each of our functions. Add the following to our front-end code, following our last section, but before our formatEverything panel end tag:

<%-- Change Password Dialog --%>
    <asp:UpdatePanel ID="upnlChangePassword" runat="server"
             ChildrenAsTriggers="true">
        <ContentTemplate>
            <asp:Panel ID="pnlChangePassword" runat="server"
                  Style="display: none" CssClass="modalPopup">
                <asp:Table runat="server">
                    <asp:TableRow>
                        <asp:TableCell HorizontalAlign="Right">
                           New Password:
                        </asp:TableCell>
                        <asp:TableCell HorizontalAlign="Left">
                           <asp:TextBox ID="txtNewPassword"
                                runat="server" TextMode="Password" />
                        </asp:TableCell>
                        <asp:TableCell>
                           <asp:RequiredFieldValidator ID="rfvNewPassword"
                                   runat="server" ControlToValidate="txtNewPassword"
                                   Text="*" Display="Dynamic"
                                   ErrorMessage="New password cannot be blank."
                                   ValidationGroup="ChangePassword" />
                        </asp:TableCell>
                    </asp:TableRow>
                    <asp:TableRow>
                        <asp:TableCell HorizontalAlign="Right">
                             Confirm Password:
                        </asp:TableCell>
                        <asp:TableCell HorizontalAlign="Left">
                            <asp:TextBox ID="txtConfirmNewPassword"
                                   runat="server" TextMode="Password" />
                            </asp:TableCell>
                        <asp:TableCell>
                           <asp:CompareValidator ID="cvConfirmPassword"
                                runat="server"
                                ControlToValidate="txtConfirmNewPassword"
                                ControlToCompare="txtNewPassword"
                                ValidationGroup="ChangePassword"
                                ErrorMessage="The passwords do not match."
                                Display="Dynamic" Text="*" />
                        </asp:TableCell>
                    </asp:TableRow>
                    <asp:TableRow>
                        <asp:TableCell ColumnSpan="3">
                            <asp:ValidationSummary id="vsChangePassword"
                                    runat="server" ValidationGroup="ChangePassword"
                                    DisplayMode="List" ShowSummary="true" 
                                    EnableClientScript="true" />
                        </asp:TableCell>
                    </asp:TableRow>
                </asp:Table>
                <br /><br />
                <asp:LinkButton ID="lnkChangePasswordSave" runat="server"
                       Text="Save" CausesValidation="true" />
                <asp:LinkButton ID="lnkChangePasswordCancel"
                       runat="server" />
            </asp:Panel>
            <asp:Button ID="btnFakeShowMPEChangePassword"
                     runat="server" style="display: none" />
            <cc1:ModalPopupExtender ID="mpeChangePassword"
                runat="server"
                TargetControlID="btnFakeShowMPEChangePassword"
                PopupControlID="pnlChangePassword"
                BackgroundCssClass="modalBackground">
            </cc1:ModalPopupExtender>
        </ContentTemplate>
    </asp:UpdatePanel>

    <%-- Confirm Reset Password Dialog --%>
    <asp:UpdatePanel ID="upnlConfirmReset" runat="server"
         ChildrenAsTriggers="true">
        <ContentTemplate>
            <asp:Panel ID="pnlConfirmResetPassword" runat="server"
                  style="display:none; text-align:center" CssClass="modalPopup">
                <asp:Label ID="lblConfirmResetPasswordInstructions"
                        runat="server" />
                <asp:Button id="btnResetPasswordYes"
                      runat="server" Text="Yes" />
                <asp:Button ID="btnResetPasswordNo"
                      runat="server" Text="No" />
            </asp:Panel>
            <asp:Button id="btnFakeMPEConfirmResetPassword"
                      runat="server" Style="display:none;" />
            <cc1:ModalPopupExtender ID="mpeConfirmResetPassword"
                runat="server"
                TargetControlID="btnFakeMPEConfirmResetPassword"
                PopupControlID="pnlConfirmResetPassword"
                CancelControlID="btnResetPasswordNo"
                BackgroundCssClass="modalBackground">
            </cc1:ModalPopupExtender>               
        </ContentTemplate>
    </asp:UpdatePanel>

    <%-- Change Security Question Dialog --%>
    <asp:UpdatePanel ID="upnlChangeSecurityQA" runat="server"
         ChildrenAsTriggers="true">
        <ContentTemplate>
            <asp:Panel ID="pnlChangeSecurityQA" runat="server"
                  Style="display:none;" CssClass="modalPopup" >
                <asp:Label ID="lblChangeSecurityQAInstructions" runat="server"
                      Text="Change your Security Question and/or Answer: " />
                <br /><br />
                <asp:Table runat="server">
                    <asp:TableRow>
                        <asp:TableCell HorizontalAlign="Right">
                           <asp:Label ID="lblChangeSecurityQuestion"
                                Text="Security Question:"
                                runat="server" />
                        </asp:TableCell>
                        <asp:TableCell HorizontalAlign="Left">
                            <asp:TextBox ID="txtSecurityQuestion" runat="server" />
                        </asp:TableCell>
                        <asp:TableCell>
                        <asp:RequiredFieldValidator runat="server"
                           Display="Dynamic" ID="rfvSecurityQuestion"
                            ControlToValidate="txtSecurityQuestion" Text="*"
                            ErrorMessage="Security Question cannot be blank."
                            ValidationGroup="SecurityQuestion" />
                        </asp:TableCell>
                    </asp:TableRow>
                    <asp:TableRow>
                        <asp:TableCell HorizontalAlign="Right">
                            <asp:Label ID="lblChangeSecurityAnswer"
                                Text="Security Answer:" runat="server" />
                        </asp:TableCell>
                        <asp:TableCell HorizontalAlign="Left">
                            <asp:TextBox ID="txtSecurityAnswer" runat="server" />
                            <cc1:TextBoxWatermarkExtender
                              ID="tbeSecurityAnswer" runat="server"
                               TargetControlID="txtSecurityAnswer"
                               WatermarkText="Enter Security Answer"
                               WatermarkCssClass="watermarked">
                            </cc1:TextBoxWatermarkExtender>
                        </asp:TableCell>
                        <asp:TableCell>
                           <asp:RequiredFieldValidator runat="server"
                               Display="Dynamic" ID="rfvSecurityAnswer"
                               ControlToValidate="txtSecurityAnswer" Text="*"
                               ErrorMessage="Security Answer cannot be blank."
                               ValidationGroup="SecurityQuestion" />
                        </asp:TableCell>
                    </asp:TableRow>
                    <asp:TableRow>
                        <asp:TableCell ColumnSpan="3">
                            <asp:ValidationSummary ID="vsSecurityQuestion"
                              runat="server" DisplayMode="List"
                               ShowSummary="true" EnableClientScript="true"
                               ValidationGroup="SecurityQuestion" />
                        </asp:TableCell>
                    </asp:TableRow>
                </asp:Table>
                <asp:LinkButton ID="lnkChangeSecurityQASave" runat="server"
                        CausesValidation="true" Text="Save" />
                <asp:LinkButton ID="lnkChangeSecurityQACancel" runat="server"
                        Text="Cancel" />
            </asp:Panel>
            <asp:Button id="btnFakeMPEChangeSecurityQuestion"
                        runat="server" Style="display:none;" />
            <cc1:ModalPopupExtender ID="mpeChangeSecurityQuestion"
                runat="server"
                TargetControlID="btnFakeMPEChangeSecurityQuestion"
                PopupControlID="pnlChangeSecurityQA"
                CancelControlID="lnkChangeSecurityQACancel"
                BackgroundCssClass="modalBackground">
            </cc1:ModalPopupExtender>
        </ContentTemplate>
    </asp:UpdatePanel>

Now let’s add functionality to use all the stuff we just added. First we need to add some to our SetUI helper subroutine. Add the following lines to our SetUIModes Enumeration:

ChangePassword
ResetPassword
ChangeSecurityQuestion

and then add the following cases to our SetUI helper Subroutine:

Case SetUIModes.ChangePassword
   mpeChangePassword.Show()
Case SetUIModes.ResetPassword
   mpeConfirmResetPassword.Show()
Case SetUIModes.ChangeSecurityQuestion
   mpeChangeSecurityQuestion.Show()

Finally, we can add functionality to make our links work. For our ‘Change Password’ link add the following:

Protected Sub lnkChangePassword_Click(ByVal sender As Object, _
             ByVal e As System.EventArgs) Handles lnkChangePassword.Click
    SetUI(SetUIModes.ChangePassword)
End Sub

Protected Sub lnkChangePasswordSave_Click(ByVal sender As Object, _
           ByVal e As System.EventArgs) Handles lnkChangePasswordSave.Click
    Dim mu As MembershipUser = Membership.GetUser(lblUsernameEdit.Text)
    Dim sOldPassword As String = mu.GetPassword()
    Try
        mu.ChangePassword(sOldPassword, txtNewPassword.Text)
        lblMessageDialog.Text = "Password successfully changed."
    Catch ex As Exception
        lblMessageDialog.Text = ex.Message.ToString()
    End Try
    SetUI(SetUIModes.MessageDialog)
End Sub

Protected Sub lnkChangePasswordCancel_Click(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles lnkChangePasswordCancel.Click
    SetUI(SetUIModes.EditUser)
End Sub

First when we click our ‘Change Password’ link, we use our SetUI function to show the modal popup for changing the password. We also define functionality for the two buttons in the change password dialog (save and cancel). When we click Cancel, we just need to set our UI back to the ‘Edit User’ dialog. When we click the save button, we want to pull the username, get the MembershipUser object associated with it and use that to change the password. If we run into an error (it’s likely due to improper password specifications – i.e. the password is not long enough, or doesn’t have enough non-alphanumeric characters etc.), we’ll just display an error message that tells the administrator the reason for failure and go back to our GridView.

Note: that the error handling here isn’t really all that clean. We’re just telling the administrator the problem and then dumping them back to the GridView rather than to the ‘Edit User’ dialog. For sake of ease, I haven’t gone into a lot of depth in the error handling, but we could devise a means of dumping the administrator back to the ‘Edit User’ dialog after the message dialog instead and create a cleaner way of checking the password’s characteristics and reporting the error to the user.

Next, we’ll handle the ‘Reset Password’' link. Add the following to your code:

Protected Sub lnkResetPassword_Click(ByVal sender As Object, _
          ByVal e As System.EventArgs) Handles lnkResetPassword.Click
    lblConfirmResetPasswordInstructions.Text = _
          "Are you sure you want to reset user: " & lblUsernameEdit.Text & _
           "'s password? This cannot be undone."
    SetUI(SetUIModes.ResetPassword)
End Sub

Protected Sub btnResetPasswordYes_Click(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles btnResetPasswordYes.Click
    Membership.GetUser(lblUsernameEdit.Text).ResetPassword()
    lblMessageDialog.Text = "Password successfully reset."
    SetUI(SetUIModes.MessageDialog)
End Sub

Protected Sub btnResetPasswordNo_Click(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles btnResetPasswordNo.Click
    SetUI(SetUIModes.EditUser)
End Sub

With the reset button, we don’t actually have a dialog to enter anything, rather we have a dialog that confirms our decision to reset the password. When we click the ‘Reset Password’ link we’ll setup our dialog with a message that’s customized with the username to be reset, and then display our confirmation message. If the administrator clicks ‘No’, then we just go back to the ‘Edit User’ dialog. If the admin clicks ‘Yes’, then we reset the user’s password and then notify the administrator that the password was successfully reset. Again, success will dump our administrator back to the GridView. We could add additional functionality to change that but I chose not to.

Lastly for this post, we’ll add functionality for changing the user’s security question. Add the following to your code:

Protected Sub lnkChangeSecurityQuestion_Click(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles lnkChangeSecurityQuestion.Click 
     txtSecurityQuestion.Text = String.Empty 
     txtSecurityAnswer.Text = String.Empty 
     SetUI(SetUIModes.ChangeSecurityQuestion) 
End Sub

Protected Sub lnkChangeSecurityQASave_Click(ByVal sender As Object, _ 
          ByVal e As System.EventArgs) Handles lnkChangeSecurityQASave.Click 
     Dim mu As MembershipUser = Membership.GetUser(lblUsernameEdit.Text) 
     mu.ChangePasswordQuestionAndAnswer(mu.GetPassword(), _
             txtSecurityQuestion.Text, txtSecurityAnswer.Text) 
     lblMessageDialog.Text = "Security Question and Answer successfully changed." 
     SetUI(SetUIModes.MessageDialog) 
End Sub

Protected Sub lnkChangeSecurityQACancel_Click(ByVal sender As Object, _ 
           ByVal e As System.EventArgs) Handles lnkChangeSecurityQACancel.Click 
     SetUI(SetUIModes.EditUser) 
End Sub

When the administrator clicks the ‘Change Security Q/A’ link, we’ll blank out fields in the Security Question form (to reset it if was used previously), and then display the dialog. If the administrator clicks ‘Cancel’, we’ll pop back to the ‘Edit User’ dialog. If the administrator clicks ‘Save’, we’ll pull the MembershipUser object for the user and then change their security question. When that is successful, we’ll display a message of success to the administrator and then pop back to the GridView.

Run you application and test out your links. If you play with your provider’s settings you’ll see that the relevant links will be enabled/disabled depending on your settings and you should be able to perform password changes and resets depending on your configurations.

Epilogue

Phew! We’ve covered a lot of ground today. We added nearly all the functionality needed to edit all aspects of our user’s account. This added a lot of code to both our front-end and back-end files (we’re not done yet). In the next segment, I’ll be talking about the one edit user piece we didn’t implement in this section, changing a username. This gets a little more complicated as it requires us to add some functionality calling directly to the database. Probably this segment was the longest code wise, from here out our code should be a little shorter, as we won’t be adding quite so much functionality at once.

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

Prologue

**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**

In previous posts, we outlined criteria in Part 1 for creating a “Better ASP.Net Member/Role Management Page”, and in Part 2 created functionality for displaying the users in our database as well as searching and filtering them. We want to continue developing our page.

Problem

In this post, we’ll be hooking up the functionality that we need to create a user in our system. Once we’ve got this functionality working, we won’t have to look outside our own page to insert users for testing purposes. We’ll use some AJAX to create a “popup” that will display a form to create our user and then use the same AJAX functionality to display a message to the administrator that the user was created successfully. Finally, we’ll add functionality to delete a user. We’ll popup a confirmation dialog for the administrator so they have to confirm user deletion.

Solution

To create a user, we have a couple of options, we can call the Membership.CreateUser method (and we create the user form-which gives us more flexibility) or we can use a CreateUserWizard control and let it do most of the work for us (including creating the form elements and performing the validation, even validating against our provider’s definition stipulations such as password strength etc.). Since Microsoft’s Web Site Admin Tool (WSAT) uses the CreateUserWizard, that’s what I used too, it reduces the amount of work that I have to do.

During this series, we’ll be creating a number of distinct pieces that we’ll be wrapping in UpdatePanels/Panel combos. The UpdatePanel so that we can AJAX-ify them and then Panels inside the UpdatePanel so that we can hide and reveal the information easily. Let’s start our create user section by adding an UpdatePanel and nesting within it a Panel control that will contain our user creation elements. At the bottom of our page (but before the end tag for our formatEverything Panel), add the following:

<%-- Create User Dialog –%>
<asp:UpdatePanel ID="upnlCreateUser" runat="server"
        ChildrenAsTriggers="true">
   <ContentTemplate>
      <asp:Panel ID="pnlCreateUser" runat="server" style="display: none;"
               CssClass="modalPopup">

      </asp:Panel>
   </ContentTemplate>
</asp:UpdatePanel>

Right off, you might notice a couple things about our Panel. We’re adding a style attribute to it, and the CssClass is being set to ‘modalPopup’. We will be extending this Panel control with a PopupControlExtender allowing it to be a modal popup when the user selects ‘Create User’. The CssClass provides this formatting. We also add a style that hides our element, rather than using the visible property because we want to be able to use the AJAX control extender on it. If we use visible and turn it to false, then the control is not rendered on the server and we will be given an error because the JavaScript has nothing to target (i.e. the control doesn’t exist once it gets to client-side).

I’m again going to use a table to do formatting (oh, I have issues with myself…) and we’ll layout our controls within that table so that our formatting is easily done. Well create a 2x2 table and use a colspan on the first row so that it becomes a header. Then we’ll use the two sides of the bottom row to display the CreateUserWizard in one, and the roles the user can be assigned to on the other side. Add the following table to your code, within our pnlCreateuser Panel:

<asp:Table runat="server" ID="tblCreateUser">
   <asp:TableRow>
      <asp:TableCell ColumnSpan="2">
         <asp:Label ID="lblCreateUserInstructions">
            Add a user: Enter the user's information and
            select their assigned roles:
         </asp:Label>
      </asp:TableCell>
   </asp:TableRow>
   <asp:TableRow>
      <asp:TableCell VerticalAlign=”Top”>

      </asp:TableCell>
      <asp:TableCell VerticalAlign=”Top”>

      </asp:TableCell>
   </asp:TableRow>
</asp:Table>

Here we create our table, we added a label to the header row, again so we can target it programmatically if we desire to do so. Now we’re ready to add with our CreateUserWizard control. In our first empty TableCell, add the following:

<asp:CreateUserWizard ID="cuwAddUser" runat="server"
      DisplayCancelButton="True"
      emailRegularExpression="\S+@\S+\.\S+"
      CancelButtonType="Link"
      FinishCompleteButtonType="Link"
      CreateUserButtonType="Link"
      LoginCreatedUser="false"
      StepPreviousButtonType="Link"
      StepPreviousButtonText="">
   <WizardSteps>
      <asp:CreateUserWizardStep runat="server"
         ID=”cuwStep1” Title="Add a new User:" />
      <asp:CompleteWizardStep runat="server" />
   </WizardSteps>
</asp:CreateUserWizard>
<br />
<asp:CheckBox ID="chkNewUserActive" runat="server"
      Checked="true" text="Active User"/>

In our CreateUserWizard, we specify that all our buttons will be links (for continuity’s sake) and that we won’t log in our user after we create them. We could use our CreateUserWizard without specifying anything in regards to the steps, but I wanted to customize the message at the top of the step, and I couldn’t do that without defining the step explicitly. Also we don’t seem to be allowed so skip the CompleteWizardStep, so it’s also included. We won’t be using it, and in fact we’ll probably have to do a workaround so that we can do things our own way (i.e. so we can skip it entirely). Also notice that the CreateWizardStep has an ID assigned. Technically we don’t need to include that, but we will so that we can use it later in the “skip CompleteWizardStep” workaround.

You’ll also notice that a checkbox is included just below our CreateUserWizard. This allows us to create a user and assign their active state all at the same time. We could modify the step’s template to include it, or add another step, but this will work just as well and we can then let the CreateUserWizard do most of the work. The one drawback is that the checkbox will end up below our Create User and Cancel links, but that’s livable in my book.

In the other TableCell, we’ll create a repeater containing roles that we can assign to our new user. Unfortunately the repeater doesn’t have any EmptyDataTemplate like our GridView does so we have to approximate one. We’ll add a label that can double as our instructions and do some handling on backside for it. Add the following between our other set of empty TableCell tags:

<asp:Label ID="lblCreateUserRolesTile" runat="server" Text="Roles" /><br>
<asp:Label id="lblCreateUserRoles" runat="server" Text="Select Roles:" />
<br><br>
<asp:Repeater ID="rptCreateUserRoles" runat="server">
   <ItemTemplate>
      <asp:CheckBox ID="chkRole" runat="server"
                     text=”<%# Container.DataItem.ToString() %>” /><br />
   </ItemTemplate>
</asp:Repeater>

Here we create two labels, one for a title, and one that we’ll use either for instructions or to notify the user that there are no roles defined to add to their user. The Repeater is pretty simple, just a checkbox in the ItemTemplate. When we access the page, the text property is bound to the Data (the role name).

Finally, we want to add what we need to make this a modal popup dialog. For this, we need to add a ModalPopupExtender. We also need to tie one of it’s parameters to a button. Since we want to control everything from code, we’ll make a “fake” button and make it invisible. Add the following just below our pnlCreateUser panel’s end tag but still within the ContentTemplate for our UpdatePanel:

<asp:Button ID="btnFakeMPECreateUser" runat="server"
      Style="display: none;" />
<cc1:ModalPopupExtender ID="mpeCreateUser" runat="server"
      BackgroundCssClass="modalBackground"
      TargetControlID="btnFakeMPECreateUser"
      PopupControlID="pnlCreateUser" >
</cc1:ModalPopupExtender>

We create a fake button and set it’s display to none, again because it has to exist and if we put visible=false, then it will not exist for the AJAX JavaScript to target. The ModalPopupExtender requires a TargetControlID (our fake button that opens the popup) and a PopupControlID (for the panel to use as the dialog). We set a background class so that all our styles can control how the background looks. There are a number of other properties that we didn’t define, if you want to know more about them you can find them on the asp.net site here.

Ok, with our front-end work done, we can get started on the back-end coding. Let’s start by hooking up our Create User link. Since we’re going to change elements on our page a number of times depending on what we are going to display, we’ll create a function that allows us to control the UI all in one place. We’ll create a SetUI subroutine and use it to do our UI shuffling. We’ll create an enumeration for it as well and use that when we call our routine. Add the following to your code behind:

Private Enum SetUIModes
   CreateUser
   ManageUsers
End Enum

Private Sub SetUI(ByVal modeToChoose As SetUIModes)
   Select Case modeToChoose
      Case SetUIModes.CreateUser
         mpeCreateUser.Show()
      Case SetUIModes.ManageUsers 
   End Select
End Sub

Basically when we create a new user, we just show our ModalPopupExtennder and viola, we have a Popup. We don’t necessarily need to worry about closing the Popup, it tends to close itself pretty easily. Next, we want to call SetUI when we click to create a user. Add the following to your lnkCreateUser_Click event handler:

SetUI(SetUIModes.CreateUser)

Try it out and you’ll see that it works. If you click Cancel, the Popup goes away, and if you click Create user you’ll get validation and the popup doesn’t go away. Next we’ll want to populate our roles list. (You may want to use the WSAT to add a role or two for the time being so that you can test things in this section). We’ll want to define a constant to target our roles data source, much like we did for the users. Add the this global constant to your code:

Private Const ROLES_DATA_SOURCE As String = "ROLES_DATA_SOURCE"

Next, we’ll create a helper routine to bind a repeater to a our roles data source, much like we did with the BindGrid routine above. We’ll may use this again so creating a BindRepeater subroutine makes sense. Add the following helper routine to your code:

Private Sub BindRepeater(ByVal theRepeater As Repeater, _
         ByVal dataSourceConstant As String)
    theRepeater.DataSource = theSession(dataSourceConstant)
    theRepeater.DataBind()
End Sub

You’ll notice that is is basically just like our BindGrid only that it accepts a repeater instead. We pull our data and bind it to grid, not much to it.

Back in our lnkCreateUser_Click event handler, we want to set our data source and then call our BindRepeater. Add the following BEFORE your SetUI statement:

SetDataSource(Roles.GetAllRoles(), ROLES_DATA_SOURCE)
BindRepeater(rptCreateUserRoles, ROLES_DATA_SOURCE)
If rptCreateUserRoles.Items.Count = 0 Then
   lblCreateUserRoles.Text = "No roles defined."
End If
chkNewUserActive.Checked = True

Run your app and you’ll see that it works. We create and then bind our data source and then we add a check to see if our data set is empty. If it is, then we want to set our instructions label to indicate that there are no roles defined. We also set the active user checkbox to checked (in case it has been unchecked previously). Now we just need to hook it up so that when the user is created, we assign the roles and stuff. First we’ll create a helper routine that allows us to pass in a repeater and have it update the user’s roles (we’ll use it again later). Add the following helper subroutine to your code:

Private Sub UpdateRoleMembership(ByVal theRepeater As Repeater, _
   byVal theUser As String)
  
If String.IsNullOrEmpty(theUser) Then Exit Sub
   For Each rptItem As RepeaterItem In theRepeater.Items
      Dim theCheckbox As CheckBox = _
             CType(rptItem.FindControl("chkRole"), CheckBox)
      If theCheckbox.Checked = True AndAlso _
                Roles.IsUserInRole(theUser, theCheckbox.Text) = False Then
         Roles.AddUserToRole(theUser, theCheckbox.Text)
      End If
      If theCheckbox.Checked = False AndAlso _
                Roles.IsUserInRole(theUser, theCheckbox.Text) = True Then
         Roles.RemoveUserFromRole(theUser, theCheckbox.Text)
      End If
   Next
End Sub

First we check that the username isn’t invalid (a null string). Then we cycle through the repeater grabbing the checkbox from the RepeaterItem each time around. If the user is already in a role, we don’t want to add it again (not a problem in this context (creating user), but may be other times we use this routine and it would give us an error) so we check that the checkbox is checked AND that the user isn’t already in the role before adding them. The same goes for removing them (again later), if the user is to be removed AND the user is currently in the role, then we remove them.

We’ll call our helper routine from our cuwCreateUser_CreatedUser cuwAddUser_CreatedUser event handler (note, created, not creating), this way we are sure not to add our roles to a user that wasn’t successfully created. Add the following inside your event handler:

Dim cuwTheUser As CreateUserWizard = CType(sender, CreateUserWizard)
UpdateRoleMembership(rptCreateUserRoles, cuwTheUser.UserName)

First we retrieve the CreateUserControl so we can access the elements within it. From it we use the username element so that we can call our UpdateRoleMembership subroutine.

We also want to set the user to active or not active depending on the user selection. You’ll remember that it isn’t part of the CreateUserWizard, but actually outside of it so we have to handle this ourselves. Add the following to your cuwCreateUser_CreatingUser cuwAddUser_CreatingUser event handler:

cuwAddUser.DisableCreatedUser = Not chkNewUserActive.Checked

Notice that we’re negating what was entered before assigning it. DisableCreatedUser is the negative property of our entry.

Now that the user has been entered, we need to update our GridView with the new user. So we reset our SetDataSource and then Bind it again. Add the following to the end of our cuwAddUser_CreatedUser event handler (after the UpdateRoleMembership call):

SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
BindGrid(gvManageUsers, USERS_DATA_SOURCE, True)

This simply resets our Data Source to all members, and then rebinds our data. Finally, we want to display a notification to the user that the user was created successfully. We’ll create a panel that we can display as a modal popup and then in our code we’ll set it’s properties and display it. For this we’ll need to flip back to our front end code and add the following at the bottom, but still within our formatEverything Panel:

<%-- Message Dialog --%>
<asp:UpdatePanel ID="upnlMessageDialog" runat="server" ChildrenAsTriggers="true">
   <ContentTemplate>
      <asp:Panel id="pnlMessageDialog" runat="server" 
               CssClass="modalPopup" style="display:none; text-align: center;" >
         <asp:Label ID="lblMessageDialog" runat="server" /> <br /> <br />
         <asp:Button ID="btnMessageDialogOk" runat="server" Text="OK" />
      </asp:Panel>
      <asp:Button ID="btnFakeMPEMessageDialog" runat="server"
                  style="display:none;" />
      <cc1:ModalPopupExtender ID="mpeMessageDialog" runat="server"
               TargetControlID="btnFakeMPEMessageDialog"
               PopupControlID="pnlMessageDialog"
               CancelControlID="btnMessageDialogOk"
               BackgroundCssClass="modalBackground">
      </cc1:ModalPopupExtender>
   </ContentTemplate>
</asp:UpdatePanel>

We’re basically just creating a panel with a label and a button. Then we’re adding a fake button for the extender and the extender and configuring it. We added one extra parameter to the extender: CancelControlID. This becomes an automatic way for us to close the modal popup without having to code it. Next, we want to go back to our code-behind and add in functionality to configure and call it.

Let’s create a helper function for setting the message as follows:

Private Sub SetMessageDialog(ByVal theMessage As String)
   lblMessageDialog.Text = theMessage
End Sub

Now we just need to call it, so add to the bottom of our cuwAddUser_CreatedUser event handler:

SetMessageDialog("User successfully created.")
SetUI(SetUIModes.MessageDialog)

We also need to implement the appropriate functionality in the SetUI routine. We need to add a case for the Message Dialog. Add ‘MessageDialog’ to your enumeration, and then add the following case to your SetUI’s case statement:

Case SetUIModes.MessageDialog
   mpeMessageDialog.Show()

Try your page, it should work: it should successfully create a user (if you successfully enter valid information) and add the user to the appropriate roles. It should also assign the user as active or disabled based on our selection. Cool. Once the user is created, you should get a message letting you know it was successfully created. You may even be able to see our new user in the GridView behind our dialog box if you’re lucky. When we click Ok, our dialog goes and we’re back to the GridView. Go ahead and click to create another user and Oi’ Vey’ what’s that?

It seems that the wonderful automation of our CreateUserWizard has managed to add a message dialog of it’s own. When we finish adding our user, it will display the final WizardStep (CompleteWizardStep). Clicking the continue button may make our dialog close (well, it doesn’t for me), but there’ isn’t anything wired up for it to do when the continue button is clicked. Usually, this leads to another page, in our case we don’t want it to, we want it to stay on our page, but we want to somehow reset the CreateUserWizard so it can be used again, and do it behind the scenes so that the administrator using it doesn’t have to deal with it. Sounds easy right?

Unfortunately, the CreateUserWizard doesn’t have a .Reset or a .Clear method, and there doesn’t seem to be a way to just click continue and have it reset (this is probably due to the control being created for regular people to enter their own information and not for administrators to enter information repeatedly), so we’ll be forced to do it the round about way and push our Wizard back a step and then clear it’s contents. (PS, if anyone can find a better way, I’d like to know about it). So, let’s create another helper subroutine to clear our Grid’s data and set back to the first step:

Private Sub cuwAddUser_Reset(ByVal theWizard As CreateUserWizard, _
            ByVal theStep As WizardStepBase)
   theWizard.MoveTo(theStep)
   theWizard.UserName = ""
   theWizard.Email = ""
   theWizard.Question = ""
End Sub

We first set the Wizard back to the first step (that’s why we assigned it an ID). Then we clear all the fields that won’t automatically clear themselves when we step back. Now we just need to call it. In my testing, I’ve found that it needs to be in the SetUI’s CreateUser block to be successful (thus it will reset just before we use it). Add the following line just before the mpeCreateUser.show() line:

cuwAddUser_Reset(cuwAddUser, cuwStep1)

We call our Routine and pass in our CreateUserWizard and the OBJECT, not the string for our CreateUserStep. Now run your application and create two users in a row. The confirmation from the CreateUserWizard should no longer be a problem.

Finally, let’s wire up the Delete User functionality. You’ve probably created enough users that you’re ready to start deleting a few of them, so let’s get started.

First we’ll want to create the confirmation dialog for our deletion. This dialog will then kick off the actual deletion of the user, depending on if yes or no was selected. So go back to our front-end markup code and add the following at the bottom:

<%-- Delete User Confirm Dialog --%>
<asp:UpdatePanel ID="upnlDeleteUserConfirm" runat="server"
            ChildrenAsTriggers="true">
   <ContentTemplate>
      <asp:Panel ID="pnlDeleteUserConfirm" runat="server"
               Style="display:none; text-align: center;" CssClass="modalPopup">
         <asp:Label ID="lblDeleteConfirmUserName"
               runat="server" visible="false" />
         <asp:Label ID="lblDeleteUserConfirmMessage" runat="server" />
         <br /><br />
         <asp:Button id="btnDeleteUserYes" runat="server" Text="Yes" />
         <asp:Button ID="btnDeleteUserNo" runat="server" Text="No" />
      </asp:Panel>
      <asp:Button id="btnFakeMPEDeleteUserConfirm" runat="server"
            Style="display:none;" />
      <cc1:ModalPopupExtender ID="mpeDeleteUserConfirm" runat="server"
            TargetControlID="btnFakeMPEDeleteUserConfirm"
            PopupControlID="pnlDeleteUserConfirm"
            CancelControlID="btnDeleteUserNo"
            BackgroundCssClass="modalBackground">
      </cc1:ModalPopupExtender>
   </ContentTemplate>
</asp:UpdatePanel>

Here we create a dialog containing our confirmation message and two buttons. You may notice that there are two labels present. The one that is hidden, allows us to pass the username to be deleted around, since it has to survive two trips to the server (once when delete is clicked and once when yes is clicked). This will be our means of state preservation. By now, you’re probably getting pretty familiar with the ModalPopupExtender, so I won’t point out much except that our CancelControlID is our No button. Much like our OK button in the message dialog, we can rely on it to close the popup and we don’t even need to clean up.

Let’s jump back to our Code-behind and wire up our events there. First we need to catch the event signifying that our Delete User link has been clicked. Our front-end defines that as gridUsersClick subroutine. We already created a stub for it earlier so that our page would work. However, before we can use it as we’d like to, we need to make some changes to the subroutine’s definition. We need to allow it to receive the normal event handler parameters. Modify your subroutine definition as follows:

Public Sub gridUsersClick(ByVal sender As Object, _
    ByVal e As CommandEventArgs)

Now we can add some functionality for deleting users. Add the following to our gridUsersClick subroutine:

If e.CommandName.Equals("DeleteUser") Then
   lblDeleteConfirmUserName.Text = CStr(e.CommandArgument)
   lblDeleteUserConfirmMessage.Text = "Are you sure you want to delete '" _
        & lblDeleteConfirmUserName.Text & "'? This cannot be undone."
    SetUI(SetUIModes.DeleteUserConfirm)
End If

We examine our CommandEventArgs for the CommandName and if it is DeleteUser, we setup our confirmation box with the username and message. When we defined our rows link, we defined that the CommandArgument should contain the username, so we’ll pull it from there. We’ll then show the confirmation using the SetUI subroutine. This means that we need to add some to our SetUI subroutine. Add an entry to your Enumeration: ‘DeleteUserConfirm’, then add the following case to our SetUI case statement:

Case SetUIModes.DeleteUserConfirm
   mpeDeleteUserConfirm.Show()

Now we just need to add some functionality to our YES button so that when the administrator selects it, our user will be deleted. Add the following to our btnDeleteUserYes_Click event handler:

Membership.DeleteUser(lblDeleteConfirmUserName.Text)
SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
BindGrid(gvManageUsers, USERS_DATA_SOURCE, False)

Once again, after we make the changes, we’ll recreate our data source and then rebind our GridView to show the changes. Run your application and test it out. You can now create and delete users at will!

Epilogue

Well, today we’ve added functionality for creating and deleting users. Currently we should have fulfilled all 6 our initial requirements except #3 and #4 (where we defined ALL the functionality). Ok, what that really means is that we’ve only finished two small pieces of our functionality from requirement #3 and haven’t even started #4. However, we do have a working control that displays all the users, filters them and we can successfully create new users and delete existing users. In the next segment, we’ll look at editing an existing user.

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

Prologue

**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**

In the last post, we got everything setup so that we can create “A Better ASP.Net Member/Role Management Page”. Today we’ll get started creating the page. If you’ve ever used Microsoft’s Web Site Administration Tool (WSAT), you’ve seen what we’re recreating.

Problem

Today we want to focus on displaying the users in our database and then additionally focus on a creating the framework for managing our users. If you look at the picture to the right, you’ll see an example of what we want to create today. If you are familiar with WSAT, you’ll notice that what we’re going to implement is a little different than the WSAT, I’ve removed the Edit Roles link. This functionality will be available in the Edit User location once we’ve implemented that piece. For now, we’ll setup the grid and test the functionality of the following features: Search for users and our GridView (retrieving and displaying users and paging in our GridView).

Solution

Before we begin, I have one piece of setup that I neglected in the last post that I want to take care of here quickly. If you are familiar with using AJAX at all, you know that we’ll need to have a ScriptManager on our page so that we can use AJAX functionality. If you notice, in our AJAX Control Toolkit we have a ToolkitScriptManager control that we can add to our page. We’ll use this one rather than the ScriptManager in the AJAX Extensions that comes with VS.

We have 2 possible locations to put this. One is in our page (that one that holds our UserControl) and the other location would in the actual UserControl. Each has it’s plusses and minuses. If we put the ScriptManager in the page (and especially into our Master Page), we’ll have AJAX capability in ALL our pages, HOWEVER, as a result, our control will not be self contained (since we have to perform outside tasks for it to work properly). If we add the ScriptManager to our UserControl, we’ll have the the AJAX functionality baked in, and be self-contained, but we’ll run the risk of having two ScriptManagers on a page if our page (or Master Page) has a ScriptManager on it. This would give us an error since we’re only allowed to have one ScriptManager per page. Since the site I’m writing this control for has the ScriptManager in our MasterPage, we’ll add the ScriptManager to our Page rather than to our UserControl. You may feel differently in your environment, but putting it in our Master Page was the best idea in ours.

Jump over to your TestUserAdminControlPage.aspx and we’ll add the ScriptManager there. Look in your AJAX Control Toolkit, and find the ToolkitScriptManager control. Drag this out into the body of our page, probably right below our form tag. It should add an entry as such:

<cc1:ToolkitScriptManager ID="ToolkitScriptManager1" runat="server">
</cc1:ToolkitScriptManager>

With setup completed, let’s get started. Jump back over to your UserAdminControl.aspx user control and switch to Source view (we’ll be editing there since we’ll be creating user TemplateFields etc.). We want to create a means of applying styles to the entirety of page, so we can consistently apply formatting (such as fonts across all the elements in our control). We’ll do this by creating a panel control and adding it to our UserControl, directly below the end </style> tab. We’ll name it pnlFormatEverything and define it as such:

<asp:Panel ID="pnlFormatEverything" runat="server" CssClass="bodyText">

</asp:Panel>

We will put EVERYTHING we do within these tags, so that our bodyText formatting will cascade down to everything. Next, we want to put an UpdatePanel that will contain our GridView. If you aren’t familiar with the UpdatePanel, it is a means of AJAX-ifying the content that resides within the panel. We will be using a number of the throughout this series, to AJAX-ify various pieces. Drag out an update panel (from the AJAX Extensions that come with VS, not the Toolkit) and put it just below the opening tag for our pnlFormatEverything panel. Define it as such:

<!-- Main User Grid --> 
<asp:UpdatePanel Runat="server" ID="upnlMainUserGrid"
        ChildrenAsTriggers="true"> 
   <ContentTemplate>

   </ContentTemplate> 
</asp:UpdatePanel>

As you may notice, the update panel has a ContentTemplate section, this is where our AJAX-ified content will go. Inside our ContentTemplate, we’ll want to create another panel control that we can make visible or invisible depending on what is being displayed. We’ll use it in the future, for right now, we just want to create it. Inside that panel, we'll create two labels, one for a title (if desired) and one for directions. We’ll use labels so that the contents can be changed programmatically if desired. Add the following inside your ContentTemplate:

<asp:Panel ID="pnlManageUsers" runat="server">
   <asp:Label ID="lblManageUsersTitle" runat="server" Text="" /><br />
   <asp:Label ID="lblManageUsersInstructions" runat="server" Text="" />
   <br /><br />

</asp:Panel>

I left the text on these blank, you’ll want to design our own text. While I put some text in mine, I left it out for brevity’s sake. Next, we’ll create the repeater that we’ll use to filter/search our user GridView. Typically I don’t like to use Tables as layout tools, but in this case I am going to make an exception. We’ll create a table that will hold all the search for users information. It will have two rows, one that’s a header and one that’s contains a repeater that has all our letters in it. Again, we’ll create our header as a label so that we can access it programmatically, if desired. In our other Row, we’ll add our Search for fields and the repeater as follows:

<asp:Table runat="server" ID="tblSearchUsers" CssClass="tableBorders" > 
   <asp:TableRow>
      <asp:TableCell CssClass="tableHeadersNTitles">
         <asp:Literal ID="lblSearchUsersHeader" runat="server"
                     Text="Search for Users" />
      </asp:TableCell>
   </asp:TableRow>
   <asp:TableRow>
      <asp:TableCell CssClass="tableBody">
         <asp:Label ID="lblSearchUsersByDropDown" runat="server"
               Text="Search by: " />
         <asp:DropDownList ID="ddlSearchUsersBy" runat="server">
            <asp:ListItem Text="Username" Selected="True" />
            <asp:ListItem Text="Email" />
         </asp:DropDownList>
         <asp:Label ID="lblSearchUsersFor" runat="server" Text=" for: " />
         <asp:TextBox ID="txtSearchUsersFor" runat="server" />
         <cc1:TextBoxWatermarkExtender ID="tweSearchUsersFor"
                  TargetControlID="txtSearchUsersFor"
                  WatermarkCssClass="watermarked"
                  WatermarkText="Enter criteria" runat="server" />
         <asp:Button ID="btnSearchUsers" runat="server" Text="Search" />
         <br />
         <asp:Label ID="lblSearchUsersWildcardsOk" runat="server" >
            Wildcard characters * and ? are permitted.
         </asp:Label> <br />
         <asp:Repeater ID="rptSearchUsersAlphabet" runat="server"
                  OnItemCommand="RetrieveLetter">
            <ItemTemplate>
               <asp:LinkButton ID="lnkSearchUsersLetter" runat="server"
                     CommandName="DisplayUsers"
                     CommandArgument="<%#Container.DataItem%>"
                     Text="<%#Container.DataItem%>" />&nbsp;
            </ItemTemplate>
         </asp:Repeater>
      </asp:TableCell>
   </asp:TableRow>
</asp:Table>
<asp:UpdateProgress runat="server" ID="upLoading">
   <ProgressTemplate>Loading...</ProgressTemplate>
</asp:UpdateProgress>
<br />

Ok, so we added quite a bit of code there, here’s what’s going on. We created a table with 2 rows, the first is just a header. The second row is where all the work goes on. We create a search line, it has a label a DropDown list (to select to search user by email or username) another label a text box to enter a search term and a button to kick off the search. We’ll have to wire up the button, but we’ll do that in a little while. Also of note, we add a TextBoxWatermarkExtender control. This is an AJAX control will extend our textbox (note the TargetControlID parameter) and enter the text in the WatermarkText parameter into the textbox. The control will take care of making it disappear when we click in the TextBox and reappear if the TextBox loses focus while the Textbox is empty (This control extender is one of the main reasons I wanted to AJAX-ify the page). We then add a little instruction letting people know that they can use wildcards in their search.

Next, we add a repeater and define it’s properties. A few items of note in regard to the repeater: we define an ItemTemplate (as you must with a repeater control) and within it we add a LinkButton. To this LinkButton, we add a CommandArgument and a CommandName. The CommandName allows us to determine what command was issued when we catch the event on backside. This can use this to uniquely define our LinkButton click from other onCommand events. We also define a CommandArgument that we’ll use to pass the actual letter that was clicked so that we can filter based on the selected letter. Both the text property and the CommandArgument property will be set to a letter when we bind our data source later (which will pretty much just be the alphabet).

Finally, just outside our table, we add an UpdateProgress control. This will notify users while we are doing an AJAX post back that something is happening. This is a pretty low key notification, but it’ll suit our purposes (likely, everything will happen so quickly you won’t see it anyhow…). You can probably add a URL via properties to a ‘loading’ graphic.

We’ll need to wire up the onCommand event and also need to create our data source and bind the repeater, we’ll get to that in a moment, first let’s finish up the front-end work for our user GridView. Let’s add a GridView to bind our users to. We’ll create a GridView and add some formatting. Once we’ve figured out our formatting, we’ll go back and insert some columns between the column tags. Create a GridView as follows:

<asp:GridView ID="gvManageUsers" runat="server"
         CellPadding="3"
         GridLines="Horizontal"
        CssClass="userGridView"
         AutoGenerateColumns="false"
         AllowPaging="true" PageSize="7"
         UseAccessibleHeader="true">
   <RowStyle CssClass="gridRowStyle" />
   <AlternatingRowStyle CssClass="gridAlternatingRowStyle" />
   <PagerStyle CssClass="gridPagerStyle" />
   <PagerSettings Mode="Numeric" />
   <HeaderStyle CssClass="tableHeadersNTitles" Font-Bold="true" />
   <Columns>

   </Columns>
</asp:GridView>

Really all this does is create and configure our Gridview and apply some formatting. Points of note would be the configuration settings for the GridView. You may notice that I have CellPadding and a CssClass assigned. I’ve had a terrible time trying to add a CellPadding trough CSS so I cheated and added this a setting. Also, notice that GridLines are set to horizontal, that way we get cleanly defined rows, but not delineated cells. We’re not automatically generating columns, and we setup paging to be 7 rows. That’s a bit of an odd number but that’s what I copied and pasted from Microsoft’s and I didn’t feel to change it. (This could easily be set on our UserControl using a property and allowing the user to customize this at design-time). Finally, we have 5 rows of stylizing for different aspects of the grid, RowStyle, AtlernateRowStyle etc. These are all being set using a CSS class, but could easily be set programmatically (at runtime) after the fact. However, since they are all distinct objects (i.e. RowStyle has child properties) we can’t set these at design time using properties. However the containing page can access a RowStyle property in its runtime code and apply styles that way, or we can do properties for each of the children.

So what good is a GridView without any data? None. Let’s create some columns so that we can display our data. It may be possible to use BoundField and CommandField columns for everything, but in my experimenting, I found it easier and more powerful to create TemplateField columns so we can work with things more easily. Between our columns tags, add the following column definitions:

<asp:TemplateField HeaderText="Active">
   <HeaderStyle HorizontalAlign="Center" />
   <ItemStyle HorizontalAlign="Center" />
   <ItemTemplate>
      <asp:CheckBox ID="chkActive"
            OnCheckedChanged="ChangeUserIsApproved" 
            runat="server"
            AutoPostBack="true" 
     Checked='<%#DataBinder.Eval(Container.DataItem, "IsApproved")%>' />
   </ItemTemplate>
</asp:TemplateField>
<asp:BoundField HeaderText="Username" DataField="Username" />
<asp:TemplateField>
   <ItemTemplate>
      <asp:LinkButton ID="lnkEditUser" runat="server"
            CommandName="EditUser" Text=”Edit User” 
         CommandArgument=’<%#DataBinder.Eval(Container.DataItem, "UserName")%>’
         OnCommand="gridUsersClick" />
   </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
   <ItemTemplate>
      <asp:LinkButton ID="lnkDeleteUser" runat="server"
            CommandName="DeleteUser" Text=”Delete User”  
            CommandArgument=’<%#DataBinder.Eval(Container.DataItem, "UserName")%>’  
            OnCommand="gridUsersClick" />
   </ItemTemplate>
</asp:TemplateField>

We setup four columns. The first is a TemplateField with a checkbox. This allows us to change whether or not the user is allowed to login. We want this to update automatically when checked rather than having to click an update button. To facilitate this functionality, we use a TemplateField. Since we can’t add a CommandArgument to a Checkbox, we’ll need to tell it to run a routine here in our markup. Normally I dislike doing things this way as it’s easy for things to get broken when we reference back-end routines in our front-end code, but our other option is to try and get the GridView to do a RowCommand and then try to pull a series of controls out of the selected row to determine what’s going on. Doing it that way is a lot more work, so we’ll do it this way.

There are three other points of interest in this column. Notice that we added centering to the style for the header and the row, and also that we are assigning the checked state of the checkbox by determining if the user “IsApproved”. Finally, notice that our Checked parameter (where IsApproved is evaluated) uses single quotes rather than double quotes. This is due to our use of double quotes within the parameter (this will be the case a number of times throughout this series, keep an eye out). This setting will come from the database when we retrieve our member data (it’s built into the Membership provider).

Our next column is the Username column,  since this column will not be doing anything except displaying data, we just used a BoundField column that binds to our “username” DataField.

Our third and fourth columns are basically the same with except they pass different CommandNames to the back-end code. We setup a linkbutton and direct it to run a function on back-end. Notice that they both run the same routine, and the only parameter that is different will be the CommandName. Again, I could have used a ButtonField, or a CommandField possibly, and then wired up with the GridView’s events on the backside, but it is much more work to pull out the information we need to process the command that way, this way it is very easy, we’ve packaged all the information right were we want it and it’s easy to get to.

To finish up our GridView, we’ll add an EmptyDataTemplate. Add this below your columns end tag:

<EmptyDataTemplate>
   <asp:Label ID="lblNoUsersMessage" runat="server" Text=""
           OnLoad="GetNoUsersText" /> <br>
</EmptyDataTemplate>

We’ll just put in a blank label and target the label at runtime if our data source is found to be empty (I’ve discussed this before). Through this we can alert the user that either there are not any users in our database, or that our search came up empty.

Finally, let’s add two links to the bottom or our page for adding a new user and for managing roles, both will not be wired up in this post, but we’ll have them for later. Add the following two LinkButtons just below your GridView’s end tag:

<br />
<asp:LinkButton ID="lnkCreateUser" runat="server" 
         Text="Create New User" />&nbsp;&nbsp;
<asp:LinkButton ID="lnkManageRoles" runat="server"
         Text="Manage Roles" />

The LinkButtons are nothing special, they just allow us to access this functionality later.

Ok, Now we’ve got our entire GridView setup, let’s get to coding it so that it does something. First thing we want to do is populate our GridView with all our users. We could assign the GridView’s DataSource directly from the Membership object, but we’ll want to alter our data source and have it remembered between pages. We have a number of ways to do that (Session, ViewState, Cookies) but since we’re already passing a lot of data over the wire as it is (because the page is so big), we’ll opt for putting it the session so it’s all kept server side. We’ll create a helper routine to set our data source to the session and another to bind a GridView to a data source. Switch over to our code view and add the following:

Private Const USERS_DATA_SOURCE As String = "USERS_DATA_SOURCE"
Private theSession As HttpSessionState = HttpContext.Current.Session

Public Sub SetDataSource(ByVal v As Object, _
          ByVal dataSourceConstant As String)
  theSession(dataSourceConstant) = v
End Sub

Public Sub BindGrid(ByVal theGrid As GridView, _
          ByVal dataSourceConstant As String, _
          ByVal bResetPageIndex As Boolean )
   If bResetPageIndex = True Then theGrid.PageIndex = 0
   theGrid.DataSource = theSession(dataSourceConstant)
   theGrid.DataBind()
End Sub

We create a constant for our session key and a constant for our session. This makes working with the session easier (less typing). Then we create a helper subroutine that we pass our dataset to and store it in our session. Finally, we create a function that we can use to bind a GridView (any GridView) to a dataset we’ve stored in our session. We’ll do it this way so that we can use it to bind other GridView objects we will be using later.

We want to load up our GridView on page load, so let’s add a page load that calls our routines and populates our GridView. Add the following into our Page_Load event:

If Not IsPostBack Then
   SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
   BindGrid(gvManageUsers, USERS_DATA_SOURCE, True)
End If

This will set our Users_Data_Source to be all members that we can retrieve from our database and will bind our GridView.

We want to populate our repeater (for filtering the users) on Page_Load as well. To do that we’ll create a helper subroutine that will populate any repeater with an alphabet (we’ll use it again later). Create the following helper routine:

Private Sub PopulateAlphabetRepeater (ByVal theRepeater As Repeater)
   Dim arr As ArrayList = New ArrayList()
   Dim chars As String = _
              (CStr("A;B;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z"))
   For Each sLettter As String In chars.Split(";")
      arr.Add(sLettter)
   Next
   arr.Add(CStr("All"))
   theRepeater.DataSource = arr
   theRepeater.DataBind()
End Sub

Basically we’re creating an array with each letter of the alphabet. Then we’re adding the ‘All’ tag to the end of the alphabet and binding it to our repeater. Now add the following line to the end of our Page_Load event:

PopulateAlphabetRepeater(rptSearchUsersAlphabet)

This will invoke our helper routine and populate our repeater. We also need to add a routine to handle our empty data set notification. For right now, we’ll just add a generic message, we may come back later and modify this functionality to provide more details (we may not) but for the time being, add the following routine to our code:

Protected Sub GetNoUsersText(ByVal sender As Object, _
     ByVal e As System.EventArgs)
   Dim lblEmptyText As Label = CType(sender, Label)
   lblEmptyText.Text = "There are no users to display."
End Sub

If you’ve ready my previous post on targeting labels in an EmptyDataTemplate, you know we’re catching the label as it’s being created and adding text to it. This allows us to create whatever message we want on the fly and display it.

Let’s also take care of our GridView’s paging. Add the following to your gvManageUsers_PageIndexChanging event handler:

gvManageUsers.PageIndex = e.NewPageIndex
BindGrid(gvManageUsers, USERS_DATA_SOURCE, False)

We also want to create a couple of stubs for our page since we won’t be completing their implementation yet, but if they don’t exist, then any builds we do will fail. So add the following to our code behind, and we’ll make them do something later:

Public Sub gridUsersClick()

End Sub

Public Sub ChangeUserIsApproved()

End Sub

These will be implemented in following segments, but for now, we’ll put them here so we can test. At this point, you should be able to run your TestUserAdminControlPage.aspx and you’ll successfully get a “Search for Users” section with the entire alphabet, and if you have any users in your database a GridView populated with users.

The final piece of functionality that we want to wire up in this segment is to create the functionality for filtering our users using the “Search for Users” section. We’ll create a helper function to do the search. Add the following subroutine:

Private Function SearchForUsers(ByVal ddlSearchMethod As DropDownList, _
       ByVal txtSearchCriteria As TextBox) As ICollection
   Dim colFoundUsers As ICollection = Nothing
   Dim sCriteria As String = txtSearchCriteria.Text

   sCriteria = sCriteria.Replace("*", "%")
   sCriteria = sCriteria.Replace("?", "_")
   If sCriteria.Trim().Length <> 0 Then
      If ddlSearchMethod.SelectedIndex = 0 Then
         colFoundUsers = Membership.FindUsersByName(sCriteria)
      Else
         colFoundUsers = Membership.FindUsersByEmail(sCriteria)
      End If
   End If

   Return colFoundUsers
End Function

We pass in the dropdown list specifying the way to search (email or username) and the TextBox with the criteria to search for. We create collection to hold the names that we find. Next we do some cleanup to the criteria and then call the correct find routine of the Membership object and return the results. Now, we need to call the routine when we click the Search button. Add the following to the btnSearchUsers_Click event handler:

SetDataSource(SearchForUsers(ddlSearchUsersBy, txtSearchUsersFor), _
         USERS_DATA_SOURCE, True)
BindGrid(gvManageUsers, USERS_DATA_SOURCE, True)

When we click the search button, we’ll set our data source equal to the filtered users collection we retrieve and then we’ll call BindGrid to rebind with our new data source. If you have any users in your DB, you should be able to successfully search through them using the criteria an filter them.

Finally, let’s get the alphabet from the repeater working. Add the following to the rptSearchUsersAlphabet_ItemCommand event handler:

Dim sLetterToFilter As String = e.CommandArgument.ToString()

If sLetterToFilter.ToLower() = "all" Then
   SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
Else
   SetDataSource(Membership.FindUsersByName(sLetterToFilter + "%"), _
          USERS_DATA_SOURCE)
End If
BindGrid(gvManageUsers, USERS_DATA_SOURCE, True)

Basically, we’ll pull the selected letter (or the word ‘all’) from the command argument and depending on which was selected, call the Membership object and return our filtered dataset and then set our data source with that. Finally we’ll rebind the grid.

That’s it for today, if you want to run the WSAT and add a couple users, you can test your functionality. It should be able to display all users and then filter them based on any input in the “Search for User” section.

Epilogue

Well, it doesn’t seem like much, but we’ve successfully started on our challenge. We’ve created a GridView with all our users and wired up filtering/searching functionality. In the next section, we’ll add functionality to the Create User link so that we can actually create users with our interface and make testing easier.

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

Prologue:

**Author’s Note** – Thank you to everyone for your comments. It has come to my attention that I didn’t pay enough attention to a couple of the details. For that I apologize, I created the code that resulted in this page and then recreated it doing optimizations as I went and converting it from a page to a user control. Sometimes I didn’t get the optimizations reflected properly in front-end or back-end code that I posted to match what’s actually happening. Also I’m sad to say I didn’t proofread well enough and have some typos. I will try to make changes to the post so they are identifiable, and post comments about the changes. Thanks for your comments, your patience, your help and most of all support. – we now continue our regularly scheduled post **

We recently finished implementing a new corporate website. We had a consultant do much of the back end coding on the site and for the most part, it’s all very good. In our specifications, we requested that we use the ASP.Net membership provider to manage user accounts. Previously our site did have some user accounts, but they were hard coded and a mess. That was acceptable at the time since they only were for site content management. In the future that won’t be acceptable since we want to create accounts for outside users and start managing parts of our site. So we had the consultants use the ASP.Net membership provider, all good and wonderful. Of all the stuff the consultants did, the one part that feels lacking was the user administration page. It just isn’t quite all that (you can see where it was copy and pasted from here). So, over the next several posts, I’ve decided to create a better/more useful membership administration page.

Problem:

If you’ve ever used Visual Studio, you may have noticed that Microsoft included an ASP.net  Web Site Administration Tool. It’s pretty useful. It’s attractively presented. It’s, well, just about what i was looking for. So I decided that I create one and I would pattern what I did after that (ok, I actually wanted to just take what they had and reuse the whole thing, but that’s not going to happen…). When you run Microsoft’s Web Site Administration Tool, you may notice that the files for it are actually on your local machine (under C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ASP.NETWebAdminFiles) Thus, you can look them up and make use of your “copy and paste inheritance.” I’m a VB person, our site is VB, and all the Microsoft files are in C#, (so I actually rewrote what was there into VB), but Microsoft included nearly all the functionality I wanted there. Granted, the files and the code flow are pretty complicated because Microsoft wanted the tool to run from any web project and with any membership provider, so they really try to be generic enough to do it.

For my purposes I wanted to make these pages a little simpler (in some ways). So after playing their files for a little bit, I came up with the following criteria for creating my Member/Role Management Page. For the purposes of this series, we will use this same set of criteria and create “A Better ASP.Net Member/Role Management Page”:

1. We want to be able to use this page with other sites if we determine to do so. Maybe not in Microsoft’s fancy way, but we want it to be easy to use and integrate with other sites, so we’ll try to make it a user control.

2. The interface should be formatted nicely: i.e. it should be easy to use and be aesthetically pleasing. While Microsoft’s ‘Web Site Administration Tool’ actually fits this bill (usable and pretty), integrating it into my existing site actually posed a number of problems: a. There are A LOT of files involved to incorporate this functionality (129 files in 19 folders to be exact). I didn’t want that many pages. Ideally I wanted one, maybe two, b. it wouldn’t integrate with our site’s look and feel, since it has it’s own master page that it ties into, with it’s own functionality, c. I found that it actually lacks some of the membership and roles functionality that I was looking for (mostly because the functionality I was looking for isn’t included in the membership/roles providers), d. it’s written in C# and our site’s in VB and e. the code flow is very complex, weaving through a number of aspx files (inline) and app_code files as well.

Thus, while my page is based on the Microsoft code and files, much of it written interpreted from scratch (is that a real phrase, interpreted from scratch) or a streamlined rewrite of their code that fits our purposes.

3. Our management page should provide the following functionality for user account (member) administration: Create, Delete, Edit and Rename. We should also be able to Change/Reset the user’s password and/or their Security Question/Answer. (BTW, some of this functionality is not available using the Membership provider (renaming) and other functionality will depend on how we configured our provider (password and security question admin) so we may need to provide work-arounds/checks to accomodate different provider configurations.

4. It should provide the following functionality for Role administration: Create, Delete, Manage Assignments, and Rename (BTW, renaming isn’t a. recommended, or b. part of the Role provider’s functionality, but I wanted to include it anyhow because it would be helpful in certain situations – look for a discussion on this in future posts, when we get to this part.).

5. Obviously I wanted it to use the ASP.net Membership and Roles providers.

6. And lastly, I wanted to incorporate some AJAX functionality. This I did for two reasons, 1. Some of the control extenders we can use to provide additional functionality to controls that I really want to have but would be a pain to do a different way (and the AJAX Control Toolkit has some easy to use VERY cool controls) and b. I wanted the page to not do a full post back whenever we made any changes to user accounts (the page will get pretty big with ALL this functionality on one page).

Ok, that doesn’t seem like much of a problem statement, so let me boil it down to this: We want to take the user administration pages that Microsoft created and repurpose them in a different language, add some functionality, put them all together into one page and then AJAX-ify them. Is that enough of a problem for you? ;-)

Solution:

I hope by now you are getting the notion that I will be breaking this down into a number of smaller posts and handling them as such. Since this is actually a pretty big undertaking we’ll be doing a lot of setup in this post to get us started and then continue on in subsequent posts adding functionality to our control. Before we get down to any real coding, we’ll need to setup the following:

First off, let’s create a new project and create a user control (named UserAdminControl.ascx) and a new page to put it on (named TestUserAdminControl.aspx). Add your user control to the test page so we have our pages setup.

Next, we need to install the AJAX Extensions Control Toolkit. If you have VS2008, you’re in luck, you’ve already got the AJAX Extensions installed, you just now need to download the control toolkit and install it. I won’t walk you through it all, but basically, you download the toolkit, extract the files somewhere meaningful (I put mine in the VS.Net Program Files folder) and add them to the toolbox. (Three addendum notes to the article: 1. The article has you add reference to the toolkit’s controls, you can actually drag and drop the .dll file from it’s final resting spot to the toolbox and do it easier (look here and go down to step 1) 2. If you add control toolkit controls to your application, directly from the Bin directory of the sample site, remove all the other language directories from the BIN folder, or they ALL will get copied to YOUR in file when you run your app (or just copy the 3 files you want out of the Bin folder somewhere else then proceed to add). Finally, if you download the newest version of the toolkit (as I did) for the AJAX control toolkit controls to work, you must have the .Net Framework 3.5 SP1 installed on your computer. If the service pack is not installed, you’ll get an error when you try to run your application)

Also, you’ll need to configure a membership provider and have a membership database. First, you have to configure your application to use forms authentication. Then setup a member and role database and membership and roles providers that can connect you to them. Again I won’t go through it all, but some articles will walk you through it can be found here, here and/or here.

Finally, we’ll setup some CSS styles that we can use as defaults for our control formatting, just as a means of making formatting easy. Remember, that since we’re creating a control, you can add properties in the code-behind page and they can then be accessible when you add your control to a page. This means that you can set properties like CSSClass and other properties and override what we put here, but for our purposes, a default is a good idea. Add the following CSS to your TestUserAdminControl.ascx page. Currently, it should probably be the only thing in the page except your @Control directive. Add the following:

<style type="text/css">

.modalBackground
{
   background-color: black;
   filter:alpha(opacity=60);
   opacity:0.6;
}

.modalPopup
{
   background-color: #6699CC;
   border-width: 3px;
   border-style: solid;
   border-color: Gray;
   padding: 3px;
}

.bodyText
{
   font-family:verdana;
   font-size: .8em;
   color: black;
   letter-spacing:0;
   line-height:150%;
   padding-left:30;
   padding-right: 20;
   padding-top: 10;
}

.tableBorders
{
   border-color: #CCDDEF;
   border-style: solid;
   border-width: 1px;
   width: 550px;
   border-spacing: 0px;
   border-collapse: collapse;
   background-color: white;
}

.tableHeadersNTitles
{
   padding: 5px;
   background-color: #6699CC;
   font-size: 0.9em;
   color: white;
   font-weight:bold;
   line-height:150%;
}

.tableBody
{
   padding: 5px;
}

.userGridView, userGridview table
{
   width: 550px;
   border-color: #CCDDEF;
   border-width: 1px;
}

.gridRowStyle
{
   background-color:#FFFFFF;
   color: black;
   padding-top: 3;
   padding-bottom: 3;
   border-color: #CADBED;
   border-style: solid;
   border-width: 1px;
}

.gridAlternatingRowStyle, .gridAlternatingRowStyle td
{
   color: black;
   letter-spacing:0;
   padding-left:30;
   padding-right: 20;
   padding-top: 3;
   padding-bottom: 3;
   background-color: #CADBED;
}

.gridPagerStyle
{
   color:#000000;
   background-color:#EEEEEE;
}

.watermarked
{
   color:gray;
}

</style>

Basically I just put together a bunch of styles to make everything look good. Some of them we won’t use for a while, but we’ll eventually use them and we’ll be ready to make everything look good like the Microsoft pages (ok, yes, I even copied their color scheme, mostly).

Epilogue:

We’ve got our work cut out for us. And while we’ve got a lot of work head of us, the coding is actually not all that complicated once we get down to it, there’s just lots of functionality we’ll be adding. We’ll be diving into the code in the next postings. But for today, we’ll call it good and just know that we’ve got everything together and setup so that we can begin our challenge.

Targeting a Control in the EmptyDataTemplate

Prologue

Recently I've been working on our new corporate website. Some of my previous posts have talked about a web service that I created to use on the new website. When I consume the web service, I've been creating a table and putting the information into the table and then binding it to a GridView. Since our web service could quite feasible dish up an empty result set, it sounds reasonable for us to use the EmptyDataTemplate to display something when there is no data.

Problem

The EmptyDataTemplate would allow us to manually enter the data that we want displayed when no data is present, however I prefer to allow messages like this to be easily customizable (I'll be using it in more than one place), so I prefer to create an entry that I can pull from the web.config. Unfortunately, you can't just target the control directly. I've run into this problem before; the control doesn't really exist until it is created, so targeting it directly won't always work. Luckily I've found a way around this that works nicely.

Solution

To get started, let's start by setting up our page. Create a new page, and call it EmptyDataTemplateTest.aspx. Switch over to design view and drop a GridView control onto the page, and name it gvwTest. Now switch over to source view and let's layout our control.

We'll make it easy, we'll instruct the GridView to generate it's own columns based on our DataSource, so really all we need to do is come up with the EmptyDataTemplate. Inside our EmptyDataTemplate, we want to create a label that we can push our dynamic text to, so our code should look like this:

<asp:gridview ID="gvwTest" runat="server" AutoGenerateColumns="True">
   <EmptyDataTemplate>
      <asp:Label ID="lblEmptyText" runat="server" />
   </EmptyDataTemplate>
</asp:gridview>

Ok, the front-end is all setup, let's setup the code-behind. Open the code-behind file and let's add some code to create a table, populate some data to it and then bind it to our GridView. Let's create a helper function to create a DataTable and create a couple entries in it. Start by importing the System.Data namespace in your project and create a new function as follows:

Public Function CreateDataSource(ByVal bEmpty As Boolean) As DataTable
    Dim dt As New DataTable()

    dt.Columns.Add(New DataColumn("FirstName", GetType(String)))
    dt.Columns.Add(New DataColumn("LastName", GetType(String)))

    If bEmpty = False Then
        Dim dr As DataRow
        dr = dt.NewRow()
        dr("FirstName") = "Sally"
        dr("LastName") = "Smith"
        dt.Rows.Add(dr)
        dr = dt.NewRow()
        dr("FirstName") = "John"
        dr("LastName") = "Doe"
        dt.Rows.Add(dr)
    End If

    Return dt
End Function

Basically, we create a DataTable, add 2 columns (FirstName and Last Name) and then based on whether we requested an empty DataSet (so it's easy to simulate empty data), we either do or don't create two rows and add them to the table. Then we return the table.

Finally to finish our setup, add the following to your Page_Load event:

gvwTest.DataSource = CreateDataSource(False)
gvwTest.DataBind()

This will assign and the bind our DataSource. Run your application and see how it works. If it works properly you should have a table with two entries. Now, let's try to assign a message to the label in our EmptyDataTemplate. Notice that it doesn't appear in our object drop down list (at the top), so how do we assign to it? Well, we have to wait for it to be loaded and the grab it on creation and add our text. Go back to your HTML code and modify your Label definition in your EmptyDataTemplate as follows:

<asp:Label ID="lblEmptyText" runat="server" OnLoad="GetEmptyText" />

Basically we're letting the control know that we want it to run a subroutine when it loads, we can't just use it's event handler like we would a normal control because it doesn't exist unless we actually DON'T have any data. (NOTE: Do NOT put a () after your GetEmptyText declaration, if you do you'll get a runtime error later about not specifying parameters - just skip the parentheses). Now let's go back to our code-behind and create the subroutine to go with it. Create a subroutine as follows (Note: this routine MUST be public or it will not work):

Public Sub GetEmptyText()
   lblEmptyText.Text = "Empty Recordset"
End Sub

Notice that we get a compile error because our label doesn't exist. So how do we get around this? Well, technically our Subroutine is the event handler for the load event SO, we can add parameters that match that of any label's load event, so let's modify our Subroutine's declaration line to be:

Public Sub GetEmptyText(ByVal sender As Object, _
    ByVal e As System.EventArgs)

So what exactly does our new declaration get us that we didn't have before? The sender object. Now you might think we need to use a FindControl() command and find our label in all the page's mess, and you might be able to do this, but there is an easier way. The sender object is really our label control in disguise, we just need to pull it together and make our change. Modify the contents of your subroutine as follows:

Dim lblEmptyText As Label = CType(sender, Label)
lblEmptyText.text = "Empty Recordset"

We created a label object and cast our sender to it and then we can access it's properties. Now when you run your application with an empty Recordset, you get your customized empty recordset message.

Epilogue

This is an easy way to enable yourself to make your message easily manageable. I use the same message in about 4 different places so it made sense to have a repository (mine's in the web.config) to pull this message from. Now I don't have to remember all the places where it is and do all the changes and hope I don't break anything, I simply change the web.config and re-upload it.

Overloading Web Services

Prologue

My last few posts have dealt with my re-purposing of an XML data feed to a web service. Honestly, this is the first time that I have created a web service that I put into production, so I haven't had a lot of experience with it. In creating the web service, I found some interesting things regarding overloading of functions in a web service.

Problem

We want to create a web service that has a function that can take any number of a few parameters. We'll continue with the train schedule example from the last post. We want to expose a function to return the Schedule i.e. GetSchedule. But, we may or may not want to pare that down using parameters for things like date, time or rail company.

Solution

We'll need to do some setup for our application, so start a new ASP.Net project. We'll need to create a web service and then consume the same service. For this article, I'm still using VS.Net 2005 so I'll actually be creating a web service rather than a WCF component. Right click and select Add New Item and add a web service. Name it OverloadingWS. We'll also create a page to consume our web service, so add a web form and name it OverloadWSTest.aspx. We will need to create a web reference to the web service so that VS will create the stubs for us. Right click and select add web reference. Select 'Web services in this solution' and select our 'OverloadingWS' service. You should notice that there is one function, and it is named 'HelloWorld'. Give the Web Reference the name of OverLoadingWSStub, and add the reference.

Ok, let's go back to our web form drop on a textbox and name it txtResults. Let's delve into some code. Go to the code behind for the web form and create global variable for our OverLoadingWSStub object. Add the following as a global variable:

Dim objOLWSStub As New OverloadingWSStub.OverloadedWS

Ok, now we're ready to start creating our web service. Let's go to the code behind for the web service and create two functions to get our schedule, one will be an overload of the other. Add the following:

<WebMethod()> _
Public Function GetSchedule() As String
   Return GetSchedule(DateTime.Now().ToString())
End Function

<WebMethod()> _
Public Function GetSchedule(ByVal aDate As String) As String
   Return "Retrieved a schedule for: " & aDate
End Function

You'll see that one function just passes an argument to the other. If you don't specify a time, then we just figure you mean right now, and pass that to the other function that does the real "processing," pretty typical for overloaded functions.

Now let's go back to our web form and call our GetSchedule() function and see what happens. Attempt to build our solution and see what happens. An error, "There was an error downloading "http:..../OverloadedWS.asmx?disco." etc. What's going on?

Well, overloading functions in a web service is more complicated than it sounds. Basically, web services name the soap messages based on the function name, and according to standards, you cannot have two soap messages with the name but have them represent different things. I found a great description of the problem in an article:

The problem is that the VisualStudio.NET tries to name the two SOAP messages the same since the SOAP message name comes from the function name. The SOAP message is the name used in the XML SOAP protocol for the method. The problem is that SOAP cannot handle duplicate message names representing different things.

So, is there a solution? Sort of. Again from the article referenced above:

However, the VS.NET IDE provides a nice workaround through the use of the "MessageName" parameter on the WebMethod attribute. This parameter changes the message name in the SOAP XML protocol so there is no conflict. We suggest that you make this name verbose so that other people that are not using VS.NET can interpret it correctly.

So, if we WANT to use overloaded functions we can, HOWEVER there is a major caveat, this only works if you only consume the web service using .Net. From another good article by Julie Lerman on the subject:

Three years ago, it seemed like a good idea to me...

However, now my perception has changed and it's important to note that just because you can do it, doesn't mean it's a good thing. It's the OO way, for sure, but it just does not jibe with messaging and contracts and it does not conform to WSI Basic profile which demands unique names for operations (web methods). So if you have any intentions of going outside of .NET with your messaging, don't do it. A contract needs to be clearly defined and by providing overloads, that just blows the contract away.

SOoooo, we can do it, but it isn't a good idea. If you want to do it, you just need to add some information to our web method declaration as follows:

<WebMethod(MessageName:="GetScheduleByDate")> _

We also need to change our WebServiceBinding at the top of our web service class so that it knows we are breaking the rules. Change the WebServiceBinding line as follows:

<WebServiceBinding(ConformsTo:=WsiProfiles.None)> _

Effectively we are just saying that our web service doesn't conform to any kind of standard. Now if we build our application, it should build and if we go back to our web form, and add a line in the page_load we can see that our function is overloaded, type the following and notice when you hit ( after GetSchedule, you get an overloaded function:

txtResults.Text = objOLWSStub.GetSchedule()

Epilogue

I was going to overload my function about a dozen different ways when I created our web service, but I found that really I wasn't following standards to do it, and like Julie mentioned in her post, then we (and anyone who takes over my coding) has to know that I broke the rules. And, who knows, perhaps we'd access the web service it from something other than .Net (such as Adobe Flash). In the end, I decided to be a good boy and NOT overload, but rather use distinct function names instead.

The Wonderful Joys of XML.SelectSingleNode or XMLDOM Snafu's

Prologue

I wrote last time about an XML data feed that we purchased access to. I've been doing a bit of development to re-purpose that feed for internal consumption. In the process, I've used the XMLDocument object to import and then repackage the feed in object format that I can easily use in a number of locations here at work. While using the XMLDocument object, I ran across a XML snafu that I remember running up against previously, so I thought I'd post on it that way I don't forget about it. The snafu specifically deals with the SelectSingleNode and SelectNodes methods.

Problem

Our task is to look at our XML document and learn specifically how to retrieve the data for each node and display it in a text box on a single row. Our sample data for this purposes has a train schedule, and 3 trains. Each train has two times associated with it, an actual time and a scheduled time. We want to retrieve these times and display them.

Solution

We'll need to do some setup for our application, so start a new ASP.Net project. We'll need our XML "data feed" so create an XML document named myDataSource.xml and add the following content:

<CurrentSchedule>
   <Train>
      <Id>1</Id>
      <DateTime>
         <Actual />
         <Time>11:30</Time>
      </DateTime>
      <DateTime>
         <Scheduled />
         <Time>11:15</Time>
      </DateTime>
   </Train>
   <Train>
      <Id>2</Id>
      <DateTime>
         <Actual />
         <Time>14:30</Time>
      </DateTime>
      <DateTime>
         <Scheduled />
      <Time>14:15</Time>
      </DateTime>
   </Train>
   <Train>
      <Id>3</Id>
      <DateTime>
         <Actual />
         <Time>18:30</Time>
      </DateTime>
      <DateTime>
         <Scheduled />
         <Time>18:15</Time>
      </DateTime>
   </Train>
</CurrentSchedule>

Basically what we have is an XML document with a root node of CurrentSchedule. Within this root node, we have multiple Train child nodes (3 in our document). Each train consists of an ID node, and two DateTime nodes. One DateTime node contains the actual time denoted by an Actual node and one with the scheduled time denoted by a Scheduled node. In the DateTime node, there is also a Time node that contains the time.

Next, create a new Web Form and name it SelectSingleNodeTest.aspx. We want to add a text box to the form that will allow us to display the information that we retrieve from our XML document. Drag a textbox on, stretch it the width of your page, name it txtResults and change the TextMode property to MultiLine.

Now we're ready to get started with the coding. We're going to need to import our XML into an XML document so we'll start by importing the XML namespace:

Imports System.Xml

Now we'll start coding in the Page_Load event to import our file and begin our parsing of the XML. Add the following to the Page_Load event:

Dim objXML As New XmlDocument()

objXML.Load(Server.MapPath("MyDataSource.xml"))
txtResults.Text = objXML.InnerXml

If you run it, you'll see that we successfully load our XML document and it displays in the text field. Now we need to begin parsing it in earnest. To do that, we want to select each Train node and then parse the different pieces of each Train and put the pieces all together on one line in the text box. Ideally, our output should resemble:

Train: 1, Actual 11:30, Scheduled: 11:15
Train: 2, Actual 14:30, Scheduled: 14:15
Train: 3, Actual 18:30, Scheduled: 18:15

Since all our nodes are the same, it figures that we should just loop through each train node. XML has a great system for selecting all the like nodes and providing an array that is easily looped through. Remove the txtResults.Text line and, add the following to your code at the end of our existing code:

Dim objTrains As XmlNodeList = objXML.SelectNodes("//Train")
For Each train As XmlNode In objTrains

Next

Now to actually parse out our information first let's select our our ID. Add the following inside your For Next loop:

txtResults.Text &= "Train: " & train.SelectSingleNode("//Id").InnerText & ", "

txtResults.Text &= vbCrLf

Basically we're taking our results box and adding the train Id. We're also adding a line return at the end of the line. We use the SelectSingleNode method to select out the ID field of our selected train, and then output the innerText. Run your app and see what results.

Train: 1,
Train: 1,
Train: 1,

Hmm.. Something's not right. It keeps printing 1 as the ID. What's going on? If you set a break point and examine the innerXML of your train node, you'll see that it display's only the XML related to the train node that we selected, so why does selecting the ID from that XML snipped return the ID of 1? Well, despite our little train node telling us that it contains only the XML for the specific train, it actually contains it all for the entire document. If you examine the ParentNode property of the train node, you'll see that it still has a link to the parent. When you perform a SelectSingleNode operation, and use the XPath "//ID", you are in essence telling it to search the ENTIRE xml document from top  to bottom and then give you the first ID node found. So guess what, it finds the ID from train #1 EVERY time (it is the first after all).

Ok, so how do we correct it? Well, we have a couple solutions. We could change our XPath so that it would only select the ID from the train node we're parsing... FYI, XPath is pretty convoluted compared to other selection languages, or we can step into the Node's items elements and draw out the information we need (much easier). Change our SelectSingleNode line so it is a follows:

txtResults.Text &= "Train: " & train.Item("Id").InnerText & ", "

Now if you run your application, it works. Since we know our Node's name (Id), we can call for it specifically and display it's contents. Run your app and you'll see that it works correctly.

Ok, now we need to parse out our times and list them. Since we have two DateTime nodes and they are in order, we can again just select our nodes and step through them like we did for the trains. Add the following between our txtResult.Text lines:

Dim objTimes As XmlNodeList = train.SelectNodes("//DateTime")
For Each time As XmlNode In objTimes
   txtResults.Text &= time.FirstChild.Name & ": " _
      & time.Item("Time").InnerText & " "
Next

We select a list of the times in our Train node and then parse through them to display all our times. You'll notice a couple things, first we can't call our 'Actual'/'Scheduled' node by name because we don't know which it will be when we iterate thorough the list. We also cannot call it by ordinal number as in item(0) because it isn't supported. Since we know it's the firstChild we call that property and then ask for it's name. We could also do some fairly robust XPath that would do a SelectSingleNode (from the very top) and retrieve our data. If we were doing our feed so that the Actual node might NOT be the first child, that's how we might have to do it. But, today we're not.

Next, we pull the time out using our Item("name").InnerText notation. Run your application and see what happens. That's right, we get all the schedules for ALL the trains. SelectNodes works much the same way as SelectSingleNode, if we ask for "//DateTime" nodes, we get everything starting from the top and coming down, 6 nodes in our case. How do we get around this? Now we'll create a bit more complex XPath.

Add a line outside your time For Next Loop as follows so that we can store the Train's Id:

Dim sTrainID As String = train.Item("Id").InnerText

We are retrieving the train's id so that we can use it later (just for more simple code). Now we want to change our dim objTimes line:

Dim objTimes As XmlNodeList = train.SelectNodes("//Train[Id=" _
  & sTrainID & "]/DateTime")

What we're doing essentially is selecting the train node with and id node that has our ID and then selecting all the DateTime nodes of that node. I won't get into all the XPath involved, that'd be another post, but essentially we specify that we want only the selected train's DateTime nodes.

Run your code and see how it works... Like a charm.

Epilogue

I don't want to get into all the XPath work arounds for our SelectNode's problems, but the important thing is that you need to remember that the way XPath works, is to traverse the ENTIRE XML Tree from the top to the bottom. Even if your node looks like it might have less than the full XML in it, Selecting in it will traverse the parent XML as well. For me, this proved a little problematic, because it was harder for me to nail down exactly why it wasn't working as I had designed it. Actually it looked like the correct data for a while, it took me a while to notice that it wasn't right. But now I know better... and so do you.

Working with Dual Monitors in .Net

Prologue

It's been a while since I posted becuase until recently I haven't had the opportunity to do much coding lately. All the website updates I was working on got suspended and then outsourced so I haven't been doing much coding, mostly other projects. Recently though, I had reason to do some work with creating a web service and some other interesting things that I may do some articles on. One of those is today's article.

We recently purchased access to an XML data feed that gives us live data that we'd like to redisplay in a number of ways and a number of locations here. So I was tasked with the coding. I'll probably talk more about our re-purposing of the feed later in a subsequent article, but today I want to talk about something different, supporting dual monitors. The data we retrieve from the feed, we will be displaying in a number of locations. This is done quite often with a dual monitor computer. One display shows at the computer, and the other at a remote monitor, usually a flat panel TV.

Problem

Our problem, we found was that the application I wrote to display the information would maximize to the primary monitor, usually we want it to be displayed on the secondary monitor instead (the flat panel TV). Our challenge in this excercise is to get the application to run maximized on the secondary monitor. Additionally, not all locations that the application will be used at should necessarily go directly to the secondary monitor, so we want to let the user who launches the application select which monitor to display on.

Solution

Ok, let's begin by creating an application that exhibits the old, undesirable primary monitor oonly behavior first and then begin modifying the application to do what we need it to do. We don't need much for an application, so it'll be pretty simple. Begin by creating a new application. Modify the form so it maximizes by default, by changing the WindowState property of the form to Maximized. Also, we wanted to NOT have any window controls showing so also set the FormBorderStyle property to none. One last thing, we want to add one line of code to the form's click event handler as follows:

Application.Exit()

This will allow us to exit the program, otherwise since we don't have a title bar at the top, it would be rather difficult. Run the application and your form should fill the primary monitor. But how do we get it to go to the secondary monitor? We can't drag it there (no title bar remember?), moving the .exe file to the second monitor doesn't help either. We'll have to do it through code. So, add to your form a button and a checkbox, name the button btnDoIt, and the checkbox chkSecondMonitor. Change the checkbox's visible property to false. Now we're ready to start adding some code to move to the 2nd monitor. I found a post on Microsoft forums that walked me through some of the code to do what we want, you can read their information here.

First we want to make the checkbox show only if there are dual monitors. If not, then we don't want to show it. So we'll add some code to detect if there are multiple monitors. Let's create a new function that returns if there are dual monitors as follows:

Public Function DualMonitors() As Boolean
   Dim myScreens() As Screen = Screen.AllScreens
   If myScreens.Length > 1 Then Return True Else Return False
End Function

Basically, this retrieves an array of all the screens (monitors) available, checks if the array has more than one element (or more than one screen) and returns true if there are multiple. Now we need to add some code to the form_load event to make visible the checkbox if there are dual monitors. Add the following to form_load:

chkSecondMonitor.Visible = DualMonitors()

Now if you run your application, you'll see that our checkbox will display depending on availability of a 2nd monitor. However, we still need to make the form move when the button is clicked (if the checkbox is checked). To our btnDoIt_clicked event, add the following:

If chkSecondMonitor.Checked = True Then
   Dim theScreen As Screen = Screen.AllScreens(1)
   Me.Location = theScreen.Bounds.Location
End If

Basically, we're retrieving the specifics of the second screen (element 1 in the AllScreens array) and using its location to set our form's location. This in effect should move our form to the starting point of the 2nd monitor, the upper-left location will become our form's upper-left location.

Run your application, check the checkbox, click the button and see what happens. That's right, big fat nothing. Why? You can't move a form that is maximized. We need to either launch the application NOT maximized, or restore (un-maximize) the form, move it and then re-maximize it. So we'll modify our code to restore, move and maximize. Add the following BEFORE the Me.Location = line in our btnDoIt_Clicked event handler:

Me.WindowState = FormWindowState.Normal

and add it's corresponding maximize call after the Me.Location line:

Me.WindowState = FormWindowState.Maximized

Now run your application and observe it functioning. Check the box, click the button, the form restores, moves and the maximizes. Just as it should.

Epilogue

Realistically speaking, moving the application to the second monitor is very easy. There are other things that we could do using the screen object that we haven't touched on (centering the form on the second monitor etc). A point of distinction that may help if you want to do further development, the Bounds encompasss the entire screen (including the task bar), if you want working area (not including the task bar) you'll need to get the WorkingArea property instead of Bounds property.

More Posts Next page »