Build a User Administration Tool: Part II

Coding the Functionality

CodeTalk

LANGUAGES: C#

ASP.NET VERSIONS: 2.0

 

Build a User Administration Tool: Part II

Coding the Functionality

 

By Bipin Joshi

 

In Part I of this series we started developing a Web user control that allows us to manage various aspects of user administration, such as password recovery, role mapping, and profile management. We configured the database and Web site for availing membership, role, and profile features of ASP.NET 2.0. Continuing our development further, we ll now code various pieces of the functionality.

 

Binding the GridView with a List of Users

ASP.NET 2.0 provides a built-in member named Membership through which membership information can be retrieved. To retrieve a list of users and bind them with the GridView we write a method named BindGrid. There are two overloads of this method, one accepting the search criteria and search by option and the other without any parameters. Figure 1 shows these two overloads.

 

private void BindGrid(string criteria,FindByOptions option)

{

MembershipUserCollection m =null;

if (option == FindByOptions.Email)

{

ViewState["emailcriteria"] = criteria;

m = Membership.FindUsersByEmail(criteria);

}

else

{

ViewState["usernamecriteria"] = criteria;

m = Membership.FindUsersByName(criteria);

}

GridView1.DataSource = m;

GridView1.DataBind();

}

private void BindGrid()

{

if (ViewState["emailcriteria"] != null)

{

BindGrid(ViewState["emailcriteria"].ToString(),

 FindByOptions.Email);

return;

}

if (ViewState["usernamecriteria"] != null)

{

BindGrid(ViewState["usernamecriteria"].ToString(),

 FindByOptions.UserName);

return;

}

MembershipUserCollection m = Membership.GetAllUsers();

GridView1.DataSource = m;

GridView1.DataBind();

}

Figure 1: Binding GridView with a list of users.

 

The first overload takes two parameters. The search criteria is a string containing the search pattern. The second parameter is of type FindByOptions. FindByOptions is an enumeration defined by us:

 

public enum FindByOptions

{

   Email,UserName

}

 

Through the enumeration we specify whether we want to find users on the basis of their e-mail address or user name. Depending on this parameter we call the FindUsersByEmail or FindUsersByName method of the Membership object. The return value of the FindUsersByEmail and FindUsersByName methods is a collection of type MembershipUserCollection. Each member of this collection is of type MembershipUser and represents a user. MembershipUserCollection acts as a data source for GridView. Note that the code creates two ViewState variables emailcriteria and usernamecriteria to store the corresponding search criterions. This way we can filter the users across postbacks. This overload of the BindGrid method is called when the administrator clicks any of the Go buttons of the Find Users panel.

 

In the second overload of the BindGrid method the code checks for existence of the same two ViewState variables. Their presence indicates that some search filter is active. If any search filter is active, the previous overload of the BindGrid is called. Otherwise, the code calls the GetAllUsers method of the Membership object. The GetAllUsers method returns all the users of the Web site in the form MembershipUserCollection. The collection is then bound with the GridView, as before. This overload of BindGrid is called from the Load event of the user control, as well as from various other places. Figure 2 shows the Page_Load event handler of the user control.

 

protected void Page_Load(object sender, EventArgs e)

{

CreateUserWizard1.ContinueDestinationPageUrl = Request.Path;

if (!IsPostBack)

  BindGrid();

}

Figure 2: The Page_Load event handler of the user control.

 

The code sets the ContinueDestinationPageUrl property of the CreateUserWizard control to the URL of the current Web form. This way the administrator is redirected to the same Web form after clicking the Continue button of the CreateUserWizard control. The code also calls the BindGrid method to bind the GridView with the list of users.

 

Handling Paging and Data Binding of the GridView

There might be many users registered with the Web site; hence, the GridView must implement paging functionality. Because we are not using any Data Source controls we need to implement paging functionality ourselves. To do so we must handle two events related to paging, PageIndexChanging and PageIndexChanged. The former is raised when a new page number is selected but before navigating to the new page. The latter is raised when the page index has been changed. Figure 3 shows the event handlers for these two events.

 

protected void GridView1_PageIndexChanging(object sender,

 GridViewPageEventArgs e)

{

GridView1.PageIndex = e.NewPageIndex;

BindGrid();

}

protected void GridView1_PageIndexChanged(object sender,

 EventArgs e)

{

Label21.Text = "";

MultiView1.ActiveViewIndex = -1;

}

Figure 3: Handling paging of the GridView.

 

The PageIndexChanging event handler receives an event argument of type GridViewPageEventArgs. The GridViewPageEventArgs class contains a property named NewPageIndex that specifies the new page index. The code sets the PageIndex property of the GridView to the value indicated in the NewPageIndex property and calls the BindGrid method.

 

In the PageIndexChanged event we clear off the MultiView by setting its ActiveViewIndex property to -1. This way no View control will be visible and we ll be saved from the mismatch between currently displayed users and user details already shown. For the same reason we also set the title label to empty string.

 

When the administrator clicks on the Show button from a row, we need to display the details about that user. That means the Click event handler of the Show button needs to know the UserName of the user that is shown by that row. To satisfy this requirement we handle the RowDataBound event of the GridView. The RowDataBound event is raised for each and every row (including header and footer rows) when the row is data bound. Figure 4 shows the RowDataBound event handler of the GridView.

 

protected void GridView1_RowDataBound(object sender,

 GridViewRowEventArgs e)

{

if (e.Row.RowType == DataControlRowType.DataRow &&

 e.Row.RowState == DataControlRowState.Normal)

{

Button b1 = (Button)e.Row.FindControl("Button1");

Label l = (Label)e.Row.FindControl("Label1");

b1.CommandName = e.Row.RowIndex.ToString();

b1.CommandArgument = l.Text;

}

}

Figure 4: Handling the RowDataBound event of the GridView.

 

We must execute our code only for the rows that contain data, i.e., excluding header and footer rows and those that are in read-only mode. This is done with the help of RowType and RowState properties, as shown. The code then retrieves a reference to the Show button and user name label by calling the FindControl method. Then the CommandName property of the button is set to the current row index. Similarly, the CommandArgument is set to the user name as displayed in the label. This way the Show button s Click event handler will know which row has been clicked and what the corresponding user name is.

 

Editing and Deleting Users

Recollect that we have Edit, Delete, Update, and Cancel buttons in the ItemTemplate and EditItemTemplate of the GridView template column. We ve set the CommandName property of these buttons to Edit, Delete, Update, and Cancel, respectively. Because of this, these buttons will raise RowEditing, RowDeleting, RowUpdating, and RowCancelingEdit events when clicked. We will handle these events in order to add editing capabilities to our GridView. Figure 5 shows the event handlers for these events.

 

protected void GridView1_RowEditing(object sender,

 GridViewEditEventArgs e)

{

GridView1.EditIndex = e.NewEditIndex;

BindGrid();

}

protected void GridView1_RowUpdating(object sender,

 GridViewUpdateEventArgs e)

{

Label l=(Label)

 GridView1.Rows[e.RowIndex].FindControl("Label1");

TextBox t1 =

  (TextBox)GridView1.Rows[e.RowIndex].FindControl("TextBox12");

TextBox t2 =

  (TextBox)GridView1.Rows[e.RowIndex].FindControl("TextBox13");

MembershipUser user = Membership.GetUser(l.Text);

user.Email = t1.Text;

user.Comment = t2.Text;

Membership.UpdateUser(user);

GridView1.EditIndex = -1;

BindGrid();

}

protected void GridView1_RowCancelingEdit(object sender,

 GridViewCancelEditEventArgs e)

{

GridView1.EditIndex = -1;

BindGrid();

}

protected void GridView1_RowDeleting(object sender,

 GridViewDeleteEventArgs e)

{

Label l =

  (Label)GridView1.Rows[e.RowIndex].FindControl("Label1");

Membership.DeleteUser(l.Text);

BindGrid();

}

Figure 5: Editing and deleting related event handlers.

 

The RowEditing event handler receives a parameter of type GridViewEditEventArgs. The GridViewEditEventArgs class contains a property named NewEditIndex that indicates the row index of the row whose Edit button has been clicked. The code sets the EditIndex property of the GridView to the value of the NewEditIndex property. Setting the EditIndex property will cause the GridView to enter in edit mode and EditItemTemplate will be displayed. The code then calls the BindGrid method to bind the GridView again.

 

The RowUpdating event is the important event wherein the actual operation of updating the user is done. The RowUpdating event receives a parameter of type GridViewUpdateEventArgs. The GridViewUpdateEventArgs class provides the row index of the row being updated via the RowIndex property. The code retrieves a reference to the row being updated from the Rows collection of the GridView. The code further obtains references to the user name label and e-mail and comment textboxes using the FindControl method. The MembershipUser object corresponding to the specified user is obtained using the GetUser method of the Membership object. The code then updates the Email and Comment properties of the MembershipUser instance and updates the user information back using the UpdateUser method of the Membership object. Finally, the EditIndex property of the GridView is set to -1 to take the GridView back into read-only mode.

 

The RowCancelingEdit event handler simply sets the EditIndex property of the GridView to -1 and binds the GridView again. This will take the GridView into read-only mode.

 

The RowDeleting event handler receives a parameter of type GridViewDeleteEventArgs. The GridViewDeleteEventArgs class supplies the RowIndex of the row whose Delete button has been clicked. This RowIndex is used to retrieve the row being deleted from the Rows collection of the GridView. Once the UserName is obtained from the user name label the code calls the DeleteUser method of the Membership object and rebinds the GridView.

 

Showing the User Information

Each row of the GridView displays a Show button in addition to Edit and Delete buttons. The administrator can select the category of information to view and click on the Show button. Figure 6 shows the outline of code that goes in the Click event handler of the Show button.

 

protected void Button1_Click1(object sender, EventArgs e)

{

Button b = (Button)sender;

MembershipUser user =

 Membership.GetUser(b.CommandArgument.ToString());

ViewState["username"] = user.UserName;

Label21.Text = "Managing Details for " + user.UserName;

DropDownList ddl = (DropDownList)GridView1.Rows[int.Parse(

 b.CommandName)].FindControl("DropDownList1");

switch (ddl.SelectedValue)

{

case "Status":

...

case "Activity":

...

case "Security":

...

case "Roles":

...

case "Profile":

...

}

}

Figure 6: The Click event handler of the Show button.

 

The code first typecasts the sender parameter into a Button. This way we get reference to the Show button that is being clicked. Recollect that we set the CommandArgument property of the Show button to the UserName. The code retrieves a MembershipUser instance by calling the GetUser method of the Membership object, passing this CommandArgument as a parameter. The MembershipUser instance is used by the remaining code to extract required information about the user. The title label shows the UserName of the user whose details are being shown. The UserName is persisted in a ViewState variable named username for later reference. Finally, there is a switch statement that tests the selection in the DropDownList control. Each case of the switch statement populates and displays the corresponding View control from the MultiView control. The code of each case is discussed next.

 

Showing User Status

The User Status panel shows whether the user is active, locked out, or on line. The case for user status contains the code shown in Figure 7.

 

...

case "Status":

CheckBox1.Checked = user.IsApproved;

CheckBox2.Checked = user.IsLockedOut;

CheckBox3.Checked = user.IsOnline;

if (user.IsLockedOut)

{

  CheckBox2.Enabled = true;

}

else

{

  CheckBox2.Enabled = false;

}

MultiView1.ActiveViewIndex = 0;

break;

...

Figure 7: The case for user status.

 

The code simply sets the checkbox values depending on the three Boolean properties of MembershipUser instance: IsApproved, IsLockedOut, and IsOnline. The user might have registered with the Web site but his account may not be activated. This is indicated by the IsApproved property. By default, when the user performs five unsuccessful login attempts his account gets marked as locked. This is indicated by the IsLockedOut property. The administrator can unlock a user only if his account is locked. Hence, the lock out checkbox is enabled only if the account is locked out. Similarly, the IsOnLine property indicates whether the user is currently logged in to the Web site. The administrator cannot change the on-line status of the user, hence the related checkbox is always disabled. After assigning the Checked property, the ActiveViewIndex property of the MultiView is set to 0. This causes the MultiView to show the User Status panel in the browser.

 

Showing User Activity

The case for user activity looks like that shown in Figure 8. The code in Figure 8 is fairly simple. It simply retrieves various activity-related properties and sets the labels accordingly. All the activity-related properties return a DateTime instance. The CreationDate property returns the date and time at which the user registered with the Web site. The LastActivityDate property indicates the date and time when the user was last authenticated. The LastLockoutDate property returns the date and time when the user account was last locked out. A user account can get locked out when the user tries to sign in unsuccessfully for the number of attempts as indicated by the MaxInvalidPasswordAttempts property of the underlying provider. The LastLoginDate property returns the date and time at which the user last logged in to the Web site. Finally, the LastPasswordChangedDate property returns the date and time when the user last changed the password. The ActiveViewIndex property of the MultiView control is set to 1 this time.

 

...

case "Activity":

Label11.Text = user.CreationDate.ToString();

Label12.Text = user.LastActivityDate.ToString();

Label13.Text = user.LastLockoutDate.ToString();

Label14.Text = user.LastLoginDate.ToString();

Label15.Text = user.LastPasswordChangedDate.ToString();

MultiView1.ActiveViewIndex = 1;

break;

...

Figure 8: The case for user activity.

 

Managing Security Details

The case for security is shown in Figure 9. The code displays the password question as entered by the user at the time of registration. Then the code checks if the password reset feature is enabled by the membership provider. This is done by checking the EnablePasswordReset property. Depending on the outcome of the check, the Reset Password button is enabled or disabled. Along the same lines, the code checks if the password retrieval feature is enabled by the membership provider. This is done by checking the EnablePasswordRetrieval property. Additionally, the code checks if the password storage format is Hashed. Hashed passwords cannot be retrieved, even if the EnablePasswordRetrieval property returns true. Accordingly, the Get Password button is enabled or disabled. Finally, the ActiveViewIndex property of the MultiView control is set to 2 so that the Security View is shown.

 

...

case "Security":

Label22.Text = user.PasswordQuestion;

if (!Membership.Provider.EnablePasswordReset)

{

Button5.Enabled = false;

}

if (!Membership.Provider.EnablePasswordRetrieval)

{

Button4.Enabled = false;

}

if (Membership.Provider.PasswordFormat ==

 MembershipPasswordFormat.Hashed)

{

Button4.Enabled = false;

Button5.Enabled = false;

}

MultiView1.ActiveViewIndex = 2;

break;

...

Figure 9: The case for security.

 

The Security panel allows the administrator to perform all four operations: change the password, change the security question and answer, retrieve the password, and reset the password. The Click event handler for the Change Password button is shown in Figure 10.

 

protected void Button6_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

bool result = user.ChangePassword(TextBox3.Text,

 TextBox4.Text);

}

Figure 10: Changing the password.

 

The code retrieves the MembershipUser instance by calling the GetUser method of the Membership object. Remember that the Click event of the Show button stores the user name in a ViewState variable named username. The same ViewState variable is used here and passed to the GetUser method. Finally, the ChangePassword method of the MembershipUser instance is called. The ChangePassword method accepts the old and new passwords and returns true or false, depending on success or failure to change the password. Figure 11 shows the Click event handler of the Change Password Q & A button.

 

protected void Button7_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

bool result =

 user.ChangePasswordQuestionAndAnswer(TextBox6.Text,

 TextBox7.Text, TextBox8.Text);

}

Figure 11: Changing the password question and answer.

 

The code retrieves the MembershipUser instance by calling the GetUser method of the Membership object. Then the ChangePasswordQuestionAndAnswer method of the MembershipUser instance is called. The ChangePasswordQuestionAndAnswer method accepts three parameters: password, new question, and new answer. The method returns true or false, thus indicating the success or failure of the operation. The Click event handler of the Get Password button is shown in Figure 12.

 

protected void Button4_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

string str = user.GetPassword(TextBox9.Text);

Label30.Text = "Password :" + str;

}

Figure 12: Retrieving the password.

 

The code retrieves the MembershipUser instance by calling the GetUser method of the Membership object. The GetPassword method is called to retrieve the password of the user. The GetPassword method accepts the password answer as a parameter and returns the password. The password is then displayed in a label. The Click event handler of the Reset Password button is shown in Figure 13.

 

protected void Button5_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

string str = user.ResetPassword(TextBox9.Text);

Label30.Text = "Password has been reset to :" + str;

}

Figure 13: Resetting the password.

 

The code retrieves the MembershipUser instance by calling the GetUser method of the Membership object, as before. Then the ResetPassword method of the MembershipUser instance is called. The method accepts the password answer as a parameter and returns the new password as a return value. The new password is then displayed in a label.

 

Managing Roles

The case for the role-management option is shown in Figure 14. The code gets a list of all the roles defined in the system by calling the GetAllRoles method of the Roles object. The returned roles are supplied as a parameter to a method named FillControlsWithRoles. This method is discussed shortly; it simply fills the CheckBoxList and DropDownList with the roles. The code then proceeds to retrieve a list of roles belonging to the user. This is done by calling the GetRolesForUser method of the Roles object. The for loop iterates through the array of roles returned by the GetRolesForUser method and checks those roles in the CheckBoxList. Finally, the ActiveViewIndex property of the MultiView is set to 3.

 

...

case "Roles":

string[] roles = Roles.GetAllRoles();

FillControlsWithRoles(roles);

string[] userroles = Roles.GetRolesForUser(user.UserName);

foreach (string s in userroles)

{

ListItem li = CheckBoxList1.Items.FindByValue(s);

if (li != null)

li.Selected = true;

}

MultiView1.ActiveViewIndex = 3;

break;

...

Figure 14: The case for roles.

 

The Role Management panel allows the administrator to perform in all three tasks user to role mapping, role creation, and role deletion. Once the administrator assigns or removes roles to a user he must click on the Update User Roles button. The Click event handler of the Update User Roles button is shown in Figure 15.

 

protected void Button10_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

foreach (ListItem li in CheckBoxList1.Items)

{

if (li.Selected)

{

if (!Roles.IsUserInRole(user.UserName, li.Value))

{

Roles.AddUserToRole(user.UserName, li.Value);

}

}

else

{

if (Roles.IsUserInRole(user.UserName, li.Value))

{

Roles.RemoveUserFromRole(user.UserName, li.Value);

}

}

}

}

Figure 15: Updating user roles.

 

The code iterates through the list of all roles i.e., CheckBoxList items and adds the user to selected roles. This is done by calling the AddUserToRole method of the Roles object. The AddUserToRole method accepts two parameters: user name and role name. Along the same lines, the user is removed from the roles unchecked by the administrator. This is achieved by calling the RemoveUserFromRole method of the Roles object. The RemoveUserFromRole method also accepts the same two parameters as the AddUserToRole method.

 

The administrator can create new roles by entering the role name in the relevant textbox and clicking the Create button. Figure 16 shows the Click event handler of the Create button.

 

protected void Button8_Click(object sender, EventArgs e)

{

Roles.CreateRole(TextBox10.Text);

FillControlsWithRoles(Roles.GetAllRoles());

}

Figure 16: Creating new roles.

 

The code simply calls the CreateRole method of the Roles object, passing the desired role name. After a new role has been added, the CheckBoxList and the DropDownList must show the new role and hence the FillControlsWithRoles method is called. The administrator can delete a role by selecting it from the relevant DropDownList and clicking the Delete button. The Click event handler of the Delete button is shown in Figure 17.

 

protected void Button9_Click(object sender, EventArgs e)

{

Roles.DeleteRole(DropDownList2.SelectedValue);

FillControlsWithRoles(Roles.GetAllRoles());

}

Figure 17: Deleting a role.

 

The code calls the DeleteRole method of the Roles object, passing the role name to be deleted. To reflect the change in the CheckBoxList and DropDownList, the FillControlsWithRoles method is called again.

 

We ve been using the FillControlsWithRoles method in many places. The code for this helper method is shown in Figure 18. The method simply clears the CheckBoxList and DropDownList control and refills them with the roles array passed as a parameter.

 

private void FillControlsWithRoles(string[] roles)

{

CheckBoxList1.Items.Clear();

DropDownList2.Items.Clear();

foreach (string s in roles)

{

CheckBoxList1.Items.Add(s);

DropDownList2.Items.Add(s);

}

}

Figure 18: Filling controls with roles.

 

This completes the user role management. Now we ll move on to coding the last feature, i.e., profile management.

 

Viewing User Profiles

The case of the profile management option is shown in Figure 19. The code retrieves the profile of the user by calling the GetProfile method. The GetProfile method accepts the user name whose profile is to be retrieved. The profile is returned as an instance of the ProfileCommon class. The code then iterates through all the profile properties using the Properties collection of the ProfileCommon class. Each element of the Properties collection is of type SettingsProperty. The name of each profile property is added to the relevant DropDownList. Note that the profile properties from a property group are shown using dot (.) notation. Finally, the ActiveViewIndex property of the MultiView is set to 4.

 

...

case "Profile":

ProfileCommon pc = Profile.GetProfile(user.UserName);

DropDownList3.Items.Clear();

foreach (SettingsProperty p in ProfileCommon.Properties)

{

DropDownList3.Items.Add(p.Name);

}

MultiView1.ActiveViewIndex = 4;

break;

...

Figure 19: The case for profile management.

 

The administrator can modify any of the profile properties or he can delete the entire profile of the user. When the administrator selects a particular profile property from the DropDownList, its value is shown a textbox. This is done in the SelectedIndexChanged event of the DropDownList (see Figure 20).

 

protected void DropDownList3_SelectedIndexChanged(

 object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

ProfileCommon pc = Profile.GetProfile(user.UserName);

object obj =

 pc.GetPropertyValue(DropDownList3.SelectedValue);

TextBox11.Text = obj.ToString();

}

Figure 20: Showing value of a profile property.

 

The code retrieves the profile of the user. The value of the selected profile property is retrieved using the GetPropertyValue method of the ProfileCommon instance. The returned value is displayed in a textbox so that the administrator can edit it, if required.

 

If the administrator changes the value of any profile property, he must click the Save button. The Save button sets the profile property to a new value and saves it in the underlying profile data store. This is shown in Figure 21.

 

protected void Button11_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

ProfileCommon pc = Profile.GetProfile(user.UserName);

object obj =

 pc.GetPropertyValue(DropDownList3.SelectedValue);

pc.SetPropertyValue(DropDownList3.SelectedValue,

 Convert.ChangeType(TextBox11.Text, obj.GetType()));

pc.Save();

}

Figure 21: Modifying a profile property.

 

The code first retrieves the value of the profile property by calling the GetPropertyValue method of the ProfileCommon instance. This is necessary because while setting the new value we need to typecast the string from the textbox to the appropriate data type. The call to the SetPropertyValue method will make this clear. The SetPropertyValue method accepts the property name and the new value as parameters. The property value parameter is of type object. While passing the new value we need to convert it into the appropriate data type, otherwise an exception will be thrown. That s why the code uses the ChangeType method of the Convert class. The ChangeType method accepts two parameters: value and the destination data type. Note that we pass the data type of the value previously retrieved here. Finally, the Save method of the ProfileCommon instance is called to persist the changes in the underlying data store.

 

To delete the complete profile of a user, the administrator can click the Delete Profile for this user LinkButton (see Figure 22).

 

protected void LinkButton2_Click(object sender, EventArgs e)

{

MembershipUser user =

 Membership.GetUser(ViewState["username"].ToString());

bool result = ProfileManager.DeleteProfile(user.UserName);

}

Figure 22: Deleting a user profile.

 

The code calls the DeleteProfile method of the ProfileManager class by passing the user name to it. The ProfileManager class is used to perform various profile-related tasks, such as deleting profiles and searching profiles.

 

That s it! Our user administration tool is ready to use. Simply drag and drop Members.ascx on the default Web form and run.

 

Conclusion

Our user administration tool allows us to manage various aspects of user administration, such as password recovery, password reset, role management, profile management, and monitoring user activity and status. In this installment we coded the functionality for the Web user control we designed in Part I. Our Web user control can be easily used on any new, as well as existing, Web forms.

 

The sample code for this series is available for download.

 

Bipin Joshi is the founder and owner of BinaryIntellect Consulting (http://www.binaryintellect.com), where he conducts professional training programs on .NET technologies. He is the author of Developer s Guide to ASP.NET 2.0 (http://www.binaryintellect.com/books) and co-author of three Wrox books on .NET 1.x. He writes regularly for http://www.DotNetBips.com, a community Web site he founded in the early days of .NET. He is a Microsoft MVP, MCAD, MCT, and member of ASPInsiders. He jots down his thoughts about .NET, life, and Yoga at http://www.bipinjoshi.com. He also conducts workshops on Yoga and Meditation, where he helps IT professionals develop a positive personality. You can contact him at mailto:[email protected].

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish