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%>" />
</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" />
<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.