Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / XAML

A Sample Silverlight 4 Application Using MEF, MVVM, and WCF RIA Services - Part 3

Rate me:
Please Sign up or sign in to vote.
4.90/5 (40 votes)
8 Jul 2011CPOL6 min read 503K   98   61
Part 3 of a series describing the creation of a Silverlight business application using MEF, MVVM Light, and WCF RIA Services. In this part, we will discuss how custom authentication, reset password, and user maintenance are implemented in the sample application.
  • Download source files and setup package from Part 1

Article Series

This article is the last part of a series on developing a Silverlight business application using MEF, MVVM Light, and WCF RIA Services.

Image 1

Contents

Introduction

In this last part, we will discuss how custom authentication, reset password, and user maintenance are implemented in this sample application. First, let's reiterate the main features we will discuss:

  • There are two types of user accounts, Admin user accounts and normal user accounts.
  • Only Admin users can add, delete, or update users through the User Maintenance screen.
  • Normal users have no access to the User Maintenance screen, and can only update their own profile.
  • After an account is added or updated, users will be prompted to reset the password and security answer when they first login.
  • If a user forgets password, the reset password screen can be used to create a new password based on the security answer.
  • If a user forgets both password and security answer, then only the Admin user can reset the password.

User, LoginUser, and PasswordResetUser

User, LoginUser and PasswordResetUser are three classes defined in the project IssueVision.Data.Web. The User class is an EntityObject class from the IssueVision Entity Model. Because the User class is defined as a partial class, we can add a few new properties as follows:

C#
/// <summary>
/// User class exposes the following data members to the client:
/// Name, FirstName, LastName, Email, Password, NewPassword,
/// PasswordQuestion, PasswordAnswer, UserType, IsUserMaintenance
/// and ProfileResetFlag
/// </summary>
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
    internal class UserMetadata
    {
        // Metadata classes are not meant to be instantiated.
        protected UserMetadata()
        {
        }

        [Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", 
                  ErrorMessageResourceType = typeof(ErrorResources))]
        [RegularExpression("^[a-zA-Z0-9_]*$", 
         ErrorMessageResourceName = "ValidationErrorInvalidUserName", 
         ErrorMessageResourceType = typeof(ErrorResources))]
        public string Name { get; set; }

        [CustomValidation(typeof(UserRules), "IsValidEmail")]
        public string Email { get; set; }

        [Exclude]
        public string PasswordAnswerHash { get; set; }

        [Exclude]
        public string PasswordAnswerSalt { get; set; }

        [Exclude]
        public string PasswordHash { get; set; }

        [Exclude]
        public string PasswordSalt { get; set; }

        [Exclude]
        public Byte ProfileReset { get; set; }
    }

    [DataMember]
    [Display(Name = "PasswordLabel", ResourceType = typeof(IssueVisionResources))]
    [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", 
              ErrorMessageResourceType = typeof(ErrorResources))]
    [RegularExpression("^.*[^a-zA-Z0-9].*$", 
        ErrorMessageResourceName = "ValidationErrorBadPasswordStrength", 
        ErrorMessageResourceType = typeof(ErrorResources))]
    [StringLength(50, MinimumLength = 12, 
        ErrorMessageResourceName = "ValidationErrorBadPasswordLength", 
        ErrorMessageResourceType = typeof(ErrorResources))]
    public string Password { get; set; }

    [DataMember]
    [Display(Name = "NewPasswordLabel", ResourceType = typeof(IssueVisionResources))]
    [Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
              ErrorMessageResourceType = typeof(ErrorResources))]
    [RegularExpression("^.*[^a-zA-Z0-9].*$", 
        ErrorMessageResourceName = "ValidationErrorBadPasswordStrength", 
        ErrorMessageResourceType = typeof(ErrorResources))]
    [StringLength(50, MinimumLength = 12, 
        ErrorMessageResourceName = "ValidationErrorBadPasswordLength", 
        ErrorMessageResourceType = typeof(ErrorResources))]
    public string NewPassword { get; set; }

    [DataMember]
    [Display(Name = "SecurityAnswerLabel", 
     ResourceType = typeof(IssueVisionResources))]
    [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", 
              ErrorMessageResourceType = typeof(ErrorResources))]
    public string PasswordAnswer { get; set; }

    [DataMember]
    public bool IsUserMaintenance { get; set; }

    [DataMember]
    public bool ProfileResetFlag
    {
        get
        {
            return this.ProfileReset != (byte)0;
        }
    }
}

From the code above, you can see that we have added some attributes through the UserMetadata class. Specifically, we excluded the properties PasswordAnswerHash, PasswordAnswerSalt, PasswordHash, PasswordSalt, and ProfileReset from being auto-generated on the client side. In addition, we have added the new properties Password, NewPassword, PasswordAnswer, and a read-only property ProfileResetFlag. These changes ensure that any password hash and password salt values only stay on the server side and never transfer through the wire.

The User class is used by the screens MyProfile and UserMaintenance, and we will go over that topic later. For now, let's examine the LoginUser and PasswordResetUser classes.

The LoginUser class is a sub-class of the User class, and implements the interface IUser. It is used within the class AuthenticationService. Following is its definition:

C#
/// <summary>
/// LoginUser class derives from User class and implements IUser interface,
/// it only exposes the following three data members to the client:
/// Name, Password, ProfileResetFlag, and Roles
/// </summary>
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(LoginUser.LoginUserMetadata))]
public sealed class LoginUser : User, IUser
{
    internal sealed class LoginUserMetadata : UserMetadata
    {
        // Metadata classes are not meant to be instantiated.
        private LoginUserMetadata()
        {
        }

        [Key]
        [Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", 
                  ErrorMessageResourceType = typeof(ErrorResources))]
        [RegularExpression("^[a-zA-Z0-9_]*$", 
         ErrorMessageResourceName = "ValidationErrorInvalidUserName", 
         ErrorMessageResourceType = typeof(ErrorResources))]
        public new string Name { get; set; }

        [Exclude]
        public new string Email { get; set; }

        [Exclude]
        public string FirstName { get; set; }

        [Exclude]
        public string LastName { get; set; }

        [Exclude]
        public string NewPassword { get; set; }

        [Exclude]
        public string PasswordQuestion { get; set; }

        [Exclude]
        public string PasswordAnswer { get; set; }

        [Exclude]
        public string UserType { get; set; }

        [Exclude]
        public bool IsUserMaintenance { get; set; }
    }

    [DataMember]
    public IEnumerable<string> Roles
    {
      get
      {
        switch (UserType)
        {
          case "A":
            return new List<string> { 
                IssueVisionServiceConstant.UserTypeUser, 
                IssueVisionServiceConstant.UserTypeAdmin };
          case "U":
            return new List<string> { "User" };
          default:
            return new List<string>();
        }
      }
      set
      {
        if (value.Contains(IssueVisionServiceConstant.UserTypeAdmin))
        {
          // Admin User
          UserType = "A";
        }
        else if (value.Contains(IssueVisionServiceConstant.UserTypeUser))
        {
          // Normal User
          UserType = "U";
        }
        else
          UserType = String.Empty;
      }
    }
}

Like in the User class, we excluded from the LoginUser class all properties from being auto-generated to the client side, except four properties: Name, Roles, Password, and ProfileResetFlag. The first two are required by the interface IUser, and the last property ProfileResetFlag is used to determine whether we need to ask the user to reset the profile after the account is newly created or recently updated by the Admin user.

Next, let's take a look at the PasswordResetUser class. This class is also a sub-class of User, and is used by the class PasswordResetService. It only exposes four properties: Name, NewPassword, PasswordQuestion, and PasswordAnswer, and is defined as follows:

C#
/// <summary>
/// PasswordRestUser derives from User class and
/// only exposes the following four data members to the client:
/// Name, NewPassword, PasswordQuestion, and PasswordAnswer
/// </summary>
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(PasswordResetUser.PasswordResetUserMetadata))]
public sealed class PasswordResetUser : User
{
    internal sealed class PasswordResetUserMetadata : UserMetadata
    {
        // Metadata classes are not meant to be instantiated.
        private PasswordResetUserMetadata()
        {
        }

        [Key]
        [Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", 
                  ErrorMessageResourceType = typeof(ErrorResources))]
        [RegularExpression("^[a-zA-Z0-9_]*$", 
         ErrorMessageResourceName = "ValidationErrorInvalidUserName", 
         ErrorMessageResourceType = typeof(ErrorResources))]
        public new string Name { get; set; }

        [DataMember]
        [Display(Name = "SecurityQuestionLabel", 
          ResourceType = typeof(IssueVisionResources))]
        public string PasswordQuestion { get; set; }

        [Exclude]
        public new string Email { get; set; }

        [Exclude]
        public string FirstName { get; set; }

        [Exclude]
        public string LastName { get; set; }

        [Exclude]
        public string Password { get; set; }

        [Exclude]
        public string UserType { get; set; }

        [Exclude]
        public bool IsUserMaintenance { get; set; }

        [Exclude]
        public bool ProfileResetFlag { get; set; }
    }
}

As we now know how the User, LoginUser, and PasswordResetUser classes are defined, we are ready to see how they are actually being used inside the AuthenticationService and PasswordResetService classes.

AuthenticationService

AuthenticationService is a DomainService class that implements the interface IAuthentication<LoginUser>, and it is the class providing custom authentication. Here is how the main function login() gets implemented:

C#
/// <summary>
/// Validate and login
/// </summary>
public LoginUser Login(string userName, string password, 
                       bool isPersistent, string customData)
{
    try
    {
        string userData;

        if (ValidateUser(userName, password, out userData))
        {
            // if IsPersistent is true, will keep logged in for up to a week 
            // (or until you logout)
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
                /* version */ 1,
                userName,
                DateTime.Now, DateTime.Now.AddDays(7),
                isPersistent,
                userData,
                FormsAuthentication.FormsCookiePath);

            string encryptedTicket = FormsAuthentication.Encrypt(ticket);
            HttpCookie authCookie = new HttpCookie(
              FormsAuthentication.FormsCookieName, encryptedTicket);

            if (ticket.IsPersistent)
            {
                authCookie.Expires = ticket.Expiration;
            }

            HttpContextBase httpContext = 
              (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
            httpContext.Response.Cookies.Add(authCookie);

            return GetUserByName(userName);
        }
        return DefaultUser;
    }
    catch (Exception ex)
    {
        Exception actualException = ex;
        while (actualException.InnerException != null)
        {
            actualException = actualException.InnerException;
        }
        throw actualException;
    }

/// <summary>
/// Validate user with password
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="userData"></param>
/// <returns></returns>
private bool ValidateUser(string username, string password, 
                          out string userData)
{
    userData = null;

    LoginUser foundUser = GetUserByName(username);

    if (foundUser != null)
    {
        // generate password hash
        string passwordHash = 
          HashHelper.ComputeSaltedHash(password, foundUser.PasswordSalt);

        if (string.Equals(passwordHash, foundUser.PasswordHash, 
                          StringComparison.Ordinal))
        {
            userData = foundUser.UserType;
            return true;
        }
        return false;
    }
    return false;
}

The Login() function calls a private function ValidateUser(), and ValidateUser() will generate a hash value based on the password the user supplied and the password salt saved in the database. If the hash value matches what is stored in the database, the user is authenticated.

PasswordResetService

Similarly, PasswordResetService is also a DomainService class. It has only two functions. The first function GetUserByName() accepts a user name as the only parameter, and returns back a valid PasswordResetUser object if the user name exists in the database. This function is called by the login screen to find out the security question before switching to the reset-password screen.

The second function is UpdateUser(). This function takes a PasswordResetUser object from the client, and checks whether the security question and answer match what is stored in the database. If they match, the new password is saved into the database as a pair of password salt and password hash.

C#
/// <summary>
/// Update user information to the database
/// User information can only be updated if the user
/// question/answer matches.
/// </summary>
[Update]
public void UpdateUser(PasswordResetUser passwordResetUser)
{
    // Search user from database by name
    User foundUser = ObjectContext.Users.FirstOrDefault(
                       u => u.Name == passwordResetUser.Name);

    if (foundUser != null)
    {
        // generate password answer hash
        string passwordAnswerHash = HashHelper.ComputeSaltedHash(
          passwordResetUser.PasswordAnswer, foundUser.PasswordAnswerSalt);

        if ((string.Equals(passwordResetUser.PasswordQuestion, 
             foundUser.PasswordQuestion, StringComparison.Ordinal)) &&
             (string.Equals(passwordAnswerHash, foundUser.PasswordAnswerHash, 
              StringComparison.Ordinal)))
        {
            // Password answer matches, so save the new user password
            // Re-generate password hash and password salt
            foundUser.PasswordSalt = HashHelper.CreateRandomSalt();
            foundUser.PasswordHash = HashHelper.ComputeSaltedHash(
                      passwordResetUser.NewPassword, foundUser.PasswordSalt);

            // re-generate passwordAnswer hash and passwordAnswer salt
            foundUser.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
            foundUser.PasswordAnswerHash = 
              HashHelper.ComputeSaltedHash(passwordResetUser.PasswordAnswer, 
              foundUser.PasswordAnswerSalt);
        }
        else
            throw new UnauthorizedAccessException(
              ErrorResources.PasswordQuestionDoesNotMatch);
    }
    else
        throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
}

So far, we have finished examining the server-side data access layer logic for custom authentication and reset password. We will switch to the client side next.

AuthenticationModel and PasswordResetModel

From the client side, the LoginForm.xaml screen binds to its ViewModel class LoginFormViewModel during runtime, and the ViewModel class has a reference to objects of AuthenticationModel and PasswordResetModel, which we will discuss now.

The AuthenticationModel class is based on the interface IAuthenticationModel defined below:

C#
public interface IAuthenticationModel : INotifyPropertyChanged
{
    void LoadUserAsync();
    event EventHandler<LoadUserOperationEventArgs> LoadUserComplete;
    void LoginAsync(LoginParameters loginParameters);
    event EventHandler<LoginOperationEventArgs> LoginComplete;
    void LogoutAsync();
    event EventHandler<LogoutOperationEventArgs> LogoutComplete;

    IPrincipal User { get; }
    Boolean IsBusy { get; }
    Boolean IsLoadingUser { get; }
    Boolean IsLoggingIn { get; }
    Boolean IsLoggingOut { get; }
    Boolean IsSavingUser { get; }

    event EventHandler<AuthenticationEventArgs> AuthenticationChanged;
}

And following is the implementation of its main function LoginAsync():

C#
/// <summary>
/// Authenticate a user with user name and password
/// </summary>
/// <param name="loginParameters"></param>
public void LoginAsync(LoginParameters loginParameters)
{
    AuthService.Login(loginParameters, LoginOperation_Completed, null);
}

The Login() function inside LoginAsync() will eventually call the server-side Login() function from the AuthenticationService class we discussed above.

Likewise, PasswordResetModel is based on the interface IPasswordResetModel.

C#
public interface IPasswordResetModel : INotifyPropertyChanged
{
    void GetUserByNameAsync(string name);
    event EventHandler<EntityResultsArgs<PasswordResetUser>> GetUserComplete;
    void SaveUserAsync();
    event EventHandler<ResultsArgs> SaveUserComplete;
    void RejectChanges();

    Boolean IsBusy { get; }
}

The function GetUserByNameAsync() gets called by the ViewModel class LoginFormViewModel when it needs to find out the right security question before switching to the reset-password screen. SaveUserAsync() is used inside ResetPasswordCommand, and it eventually calls the server-side UpdateUser() from the PasswordResetService class to verify and save a new password if both the security question and answer match what is in the database.

This concludes our discussion about custom authentication and reset password logic. Next, let's look into how user maintenance is done.

My Profile Screen

As we stated above, the My Profile screen uses the User class. This screen binds to the ViewModel class MyProfileViewModel, which retrieves and updates user information through two server-side functions GetCurrentUser() and UpdateUser() from the IssueVisionService class.

Also, during the first successful login after an account has been updated or added by the Admin user, the My Profile screen will be shown instead of the Home page:

Image 2

The actual logic to implement this resides in the ViewModel class MainPageViewModel, and is as follows:

C#
private void _authenticationModel_AuthenticationChanged(object sender, 
             AuthenticationEventArgs e)
{
    IsLoggedIn = e.User.Identity.IsAuthenticated;
    IsLoggedOut = !(e.User.Identity.IsAuthenticated);
    IsAdmin = e.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin);

    if (e.User.Identity.IsAuthenticated)
    {
        WelcomeText = "Welcome " + e.User.Identity.Name;
        // if ProfileResetFlag is set
        // ask the user to reset profile first
        if (e.User is LoginUser)
        {
            if (((LoginUser)e.User).ProfileResetFlag)
            {
                // open the MyProfile screen
                AppMessages.ChangeScreenMessage.Send(ViewTypes.MyProfileView);
                CurrentScreenText = ViewTypes.MyProfileView;
            }
            else
            {
                // otherwise, open the home screen
                AppMessages.ChangeScreenMessage.Send(ViewTypes.HomeView);
                CurrentScreenText = ViewTypes.HomeView;
            }
        }
    }
    else
        WelcomeText = string.Empty;
}

User Maintenance Screen

Lastly, we will talk about the User Maintenance screen. This screen is only available to Admin users. It binds to the ViewModel class UserMaintenanceViewModel, and eventually retrieves and updates user information through the functions GetUsers(), InsertUser(), UpdateUser(), and DeleteUser() from the IssueVisionService class on the server side. Let's check how the function InsertUser() is implemented:

C#
public void InsertUser(User user)
{
    // check for insert user permission
    if (CheckUserInsertPermission(user) && user.IsUserMaintenance)
    {
        // validate whether the user already exists
        User foundUser = ObjectContext.Users.Where(
            n => n.Name == user.Name).FirstOrDefault();
        if (foundUser != null)
            throw new ValidationException(ErrorResources.CannotInsertDuplicateUser);

        // Re-generate password hash and password salt
        user.PasswordSalt = HashHelper.CreateRandomSalt();
        user.PasswordHash = HashHelper.ComputeSaltedHash(
                             user.NewPassword, user.PasswordSalt);

        // set a valid PasswordQuestion
        SecurityQuestion securityQuestion = 
          ObjectContext.SecurityQuestions.FirstOrDefault();
        if (securityQuestion != null)
            user.PasswordQuestion = securityQuestion.PasswordQuestion;
        // set PasswordAnswer that no body knows
        user.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
        user.PasswordAnswerHash = HashHelper.CreateRandomSalt();

        // requires the user to reset profile
        user.ProfileReset = 1;

        if ((user.EntityState != EntityState.Detached))
        {
            ObjectContext.ObjectStateManager.ChangeObjectState(
                               user, EntityState.Added);
        }
        else
        {
            ObjectContext.Users.AddObject(user);
        }
    }
    else
        throw new ValidationException(ErrorResources.NoPermissionToInsertUser);
}

From the code above, we can see that no security answer is actually set when a new user is first created. This is one of the reasons that users are reminded to reset their profile during the first login.

Further Work

This concludes our discussion. There is, of course, further work needed to improve this sample application. One of the obvious and (intentional) omissions is the unit test project. Also, adding a logging mechanism will help trace any potential problems.

I hope you find this article series useful, and please rate and/or leave feedback below. Thank you!

References

History

  • May 2010 - Initial release
  • July 2010 - Minor update based on feedback
  • November 2010 - Update to support VS2010 Express Edition
  • February 2011 - Update to fix multiple bugs including memory leak issues
  • July 2011 - Update to fix multiple bugs

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
GeneralMy vote of 5 Pin
Jerry_zh1-Mar-12 6:17
Jerry_zh1-Mar-12 6:17 
QuestionHi Pin
Goran _21-Feb-12 13:34
Goran _21-Feb-12 13:34 
AnswerRe: Hi Pin
Weidong Shen21-Feb-12 16:00
Weidong Shen21-Feb-12 16:00 
GeneralRe: Hi Pin
Goran _21-Feb-12 23:42
Goran _21-Feb-12 23:42 
GeneralRe: Hi Pin
Weidong Shen22-Feb-12 3:16
Weidong Shen22-Feb-12 3:16 
QuestionNot able to see the login screen. Pin
Lau Kok Yoon17-Dec-11 23:56
Lau Kok Yoon17-Dec-11 23:56 
AnswerRe: Not able to see the login screen. Pin
Weidong Shen18-Dec-11 9:48
Weidong Shen18-Dec-11 9:48 
GeneralRe: Not able to see the login screen. Pin
Lau Kok Yoon18-Dec-11 19:12
Lau Kok Yoon18-Dec-11 19:12 
QuestionRe: Not able to see the login screen. Pin
Lau Kok Yoon18-Dec-11 19:59
Lau Kok Yoon18-Dec-11 19:59 
GeneralRe: Not able to see the login screen. Pin
Lau Kok Yoon18-Dec-11 21:02
Lau Kok Yoon18-Dec-11 21:02 
GeneralRe: Not able to see the login screen. Pin
Weidong Shen19-Dec-11 3:31
Weidong Shen19-Dec-11 3:31 
QuestionThe name does not exist in the context. How to get rid of this error? Pin
Lau Kok Yoon14-Dec-11 0:08
Lau Kok Yoon14-Dec-11 0:08 
AnswerRe: The name does not exist in the context. How to get rid of this error? Pin
Weidong Shen14-Dec-11 7:50
Weidong Shen14-Dec-11 7:50 
GeneralRe: The name does not exist in the context. How to get rid of this error? Pin
Lau Kok Yoon15-Dec-11 20:26
Lau Kok Yoon15-Dec-11 20:26 
GeneralRe: The name does not exist in the context. How to get rid of this error? Pin
Weidong Shen16-Dec-11 4:31
Weidong Shen16-Dec-11 4:31 
GeneralRe: The name does not exist in the context. How to get rid of this error? Pin
Lau Kok Yoon16-Dec-11 22:16
Lau Kok Yoon16-Dec-11 22:16 
GeneralRe: The name does not exist in the context. How to get rid of this error? Pin
Lau Kok Yoon16-Dec-11 23:03
Lau Kok Yoon16-Dec-11 23:03 
GeneralRe: The name does not exist in the context. How to get rid of this error? Pin
Weidong Shen17-Dec-11 11:53
Weidong Shen17-Dec-11 11:53 
To add XAP files to ClientBin, you need to right-click on the web project and select "Properties". Then, select "Silverlight Applications" tab, and then add existing Silverlight project in the solution.

Thanks,
GeneralMy vote of 5 Pin
Lau Kok Yoon13-Dec-11 23:44
Lau Kok Yoon13-Dec-11 23:44 
QuestionValidate if record exists before cretaitng it Pin
Tsubaza12-Oct-11 2:50
Tsubaza12-Oct-11 2:50 
AnswerRe: Validate if record exists before cretaitng it Pin
Weidong Shen12-Oct-11 3:51
Weidong Shen12-Oct-11 3:51 
GeneralRe: Validate if record exists before creating it and also Update problem Pin
Tsubaza12-Oct-11 8:03
Tsubaza12-Oct-11 8:03 
GeneralRe: Validate if record exists before creating it and also Update problem Pin
Weidong Shen13-Oct-11 3:56
Weidong Shen13-Oct-11 3:56 
GeneralRe: Validate if record exists before creating it and also Update problem Pin
Tsubaza13-Oct-11 6:06
Tsubaza13-Oct-11 6:06 
GeneralRe: Validate if record exists before creating it and also Update problem Pin
Weidong Shen13-Oct-11 6:40
Weidong Shen13-Oct-11 6:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.