Click here to Skip to main content
Click here to Skip to main content
Go to top

A Silverlight Sample Built with Self-Tracking Entities and WCF Services - Part 3

, 8 Mar 2011
Rate this:
Please Sign up or sign in to vote.
Part 3 of a series describing the creation of a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light Toolkit, MEF, and T4 Templates.
  • Please visit this project site for the latest releases and source code.

Article Series

This article is the third part of a series on developing a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light Toolkit, MEF, and T4 Templates.

Contents

Introduction

In this third part, we will focus on data validation with self-tracking entities. The purpose of using data validation is to make sure that any data is validated before being stored in the database. It provides users with necessary guidance during their data input tasks and is an important part of any Silverlight LOB application. In the first half of this article, we will discuss different approaches of adding data validation on both client and server sides. After that, we will dig a little deeper to show you how this data validation infrastructure is actually implemented.

How to Add a Validation Attribute

WCF RIA Services has built-in support for member/field level validation, entity level validation, and operation level validation. Validation attributes are from the System.ComponentModel.DataAnnotations namespace, and they include:

  • RequiredAttribute: specifies that a data field value is required
  • StringLengthAttribute: specifies the minimum and maximum length of characters that are allowed in a data field
  • RegularExpressionAttribute: specifies that a data field value must match the specified Regular Expression
  • RangeAttribute: specifies the numeric range constraints for the value of a data field
  • CustomValidationAttribute: specifies a custom validation method to validate a property or class instance

For the EF auto-generated entity classes, we can add validation attributes on the properties of the metadata class like follows:

[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
  internal class UserMetadata
  {
    // Metadata classes are not meant to be instantiated.
    protected UserMetadata()
    {
    }
    ......
    [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; }
    ......
  }
}

The validation attributes on metadata classes will be used for server side validation, and WCF RIA Services will then generate these same validation attributes when client side entity classes are being auto-generated. This makes sure that the same validation logic is shared on both sides.

Unfortunately, the metadata class approach does not work for our sample because we are sharing auto-generated self-tracking entity classes, and a metadata class can only exist on the server side, not the client side. We have to take a different approach by using a VS2010 extension called "Portable Extensible Metadata". It will add metadata to our EF model and let IssueVisionModel.tt auto-generate all the data annotation attributes on the property level for each self-tracking entity class.

Before we discuss how to add metadata to the EF model, let us cover how to add client-side only validation first.

Adding Client-side Only Validation

Client-side only validation applies to client-side only properties defined under the ClientExtension folder of the project IssueVision.Data. One of the examples is the PasswordConfirmation property of class PasswordResetUser.

/// <summary>
/// PasswordResetUser class client-side extensions
/// </summary>
public partial class PasswordResetUser
{
  ......
  [Display(Name = "Confirm password")]
  [Required(ErrorMessage = "This field is required.")]
  [CustomValidation(typeof(PasswordResetUser), "CheckPasswordConfirmation")]
  public string PasswordConfirmation
  {
    get { return this._passwordConfirmation; }
    set
    {
      if (_passwordConfirmation != value)
      {
        PropertySetterEntry("PasswordConfirmation");
        _passwordConfirmation = value;
        PropertySetterExit("PasswordConfirmation", value);
        OnPropertyChanged("PasswordConfirmation");
      }
    }
  }
  private string _passwordConfirmation;

  /// <summary>
  /// Custom validation of whether new password and confirm password match
  /// </summary>
  /// <param name="passwordConfirmation"></param>
  /// <param name="validationContext"></param>
  /// <returns></returns>
  public static ValidationResult CheckPasswordConfirmation(
         string passwordConfirmation, ValidationContext validationContext)
  {
    PasswordResetUser currentUser = 
      (PasswordResetUser)validationContext.ObjectInstance;

    if (!string.IsNullOrEmpty(currentUser.ActualPassword) &&
        !string.IsNullOrEmpty(passwordConfirmation) &&
        currentUser.ActualPassword != passwordConfirmation)
    {
      return new ValidationResult("Passwords do not match.", 
             new string[] { "PasswordConfirmation" });
    }

    return ValidationResult.Success;
  }
}

Since the property PasswordConfirmation is only defined on the client side, the validation logic only applies to the client side. And, because the property is not generated by a T4 template, we can simply add data annotation attributes directly to the property itself, including the CustomValidation attribute.

Adding Validation Through Entity Data Model Designer

Next, we will use the NewPassword property of the User class as an example, and walk through the steps to add any necessary data annotation attributes using the Entity Data Model Designer. Here is a list of validation requirements for the NewPassword property:

  • The new password is a required property.
  • The password must be at least 12 and at most 50 characters long.
  • The password needs to contain at least one special character, e.g., @ or #

First, open the Entity Data Model Designer of IssueVision.edmx and select the NewPassword property of the entity User. From the Property window (as shown below), we can specify "New Password" as its display name.

Next, open the "Validations Editor" window by selecting the collection of "Validations" (highlighted above) and add the schema metadata for the three validation conditions.

After saving our changes for the EDM file IssueVision.edmx, the T4 template will automatically generate all self-tracking entity classes with the new data annotation attributes we just added.

One of the limitations of adding validation metadata through the Entity Data Model Designer is that it does not currently support CustomValidationAttribute. For custom validation support, we have to take a different approach as described below.

Adding Entity Level CustomValidation

For entity level custom validation, we can simply take the partial class approach and add CustomValidation attributes directly. In our sample application, entity level custom validation logic is located inside the validation folders. Specifically, on the server side, they are under the validation folder of the project IssueVision.Data.Web; on the client side, they are under the validation folder of the project IssueVision.Data. Following is an example from the class Issue, and we are adding two CustomValidation attributes to the class itself.

/// <summary>
/// Issue class validation logic
/// </summary>
[CustomValidation(typeof(IssueRules), "CheckStatusActive")]
[CustomValidation(typeof(IssueRules), "CheckStatusOpen")]
public partial class Issue
{
    ......
}

The related custom validation functions, CheckStatusActive() and CheckStatusOpen(), are defined in a separate static class called IssueRules:

public static partial class IssueRules
{
  ......
  /// <summary>
  /// When status is Active, assigned-to-user cannot be null.
  /// </summary>
  /// <param name="issue"></param>
  /// <param name="validationContext"></param>
  /// <returns></returns>
  public static ValidationResult CheckStatusActive(Issue issue, 
                ValidationContext validationContext)
  {
    if (issue.StatusID == IssueVisionServiceConstant.ActiveStatusID && 
        issue.AssignedToID == null)
      return new ValidationResult("A user is required when the " + 
             "status is Active. ", 
             new string[] { "StatusID", "AssignedToID" });

    return ValidationResult.Success;
  }

  /// <summary>
  /// When status is Open, assigned-to-user should be null.
  /// </summary>
  /// <param name="issue"></param>
  /// <param name="validationContext"></param>
  /// <returns></returns>
  public static ValidationResult CheckStatusOpen(Issue issue, 
                ValidationContext validationContext)
  {
    if (issue.StatusID == IssueVisionServiceConstant.OpenStatusID && 
                          issue.AssignedToID != null)
      return new ValidationResult("AssignedTo user is not needed when " + 
             "the status is Open.", 
             new string[] { "StatusID", "AssignedToID" });

    return ValidationResult.Success;
  }
}

From the code snippet above, we can see that the custom validation functions first check whether specified conditions are met and return ValidationResult.Success if everything is OK; otherwise, they return a new ValidationResult object with an error message. The second parameter of the ValidationResult object is a string array of all affected properties. For the function CheckStatusActive(), the affected properties are StatusID and AssignedToID. This means that both of these two properties would be highlighted if validation fails.

Adding Property Level CustomValidation

After going over the topic of how to add entity level custom validation, let us take a look at how to define custom validation on property level. Property level custom validation functions are also defined inside the validation folders on both client and server sides. Following is an example about the Email property of the User class, and we need to make sure that the email address is valid during user input tasks.

/// <summary>
/// User class validation logic
/// </summary>
public partial class User
{
  /// <summary>
  /// Partial method to add all validation actions
  /// defined for this class
  /// </summary>
  partial void InitializeValidationSettings()
  {
  #if SILVERLIGHT
    this.ValidateEntityOnPropertyChanged = false;
  #endif
    AddPropertyValidationAction("Email", ValidateEmail);
  }

  #region "Private Validation Methods"

  /// <summary>
  /// Validation Action:
  /// check whether the email address is valid or not
  /// </summary>
  /// <param name="value"></param>
  private void ValidateEmail(object value)
  {
    string email = value as string;

    // user Email can be null
    if (email == null) return;
    // validate the e-mail format.
    if (Regex.IsMatch(email,
          @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$" + 
          @"%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" +
          @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z]" + 
          @"[-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$") == false)
    {
    #if SILVERLIGHT
      ValidationResult result = new ValidationResult(
         "Invalid email address.", new List<string> { "Email" });
      this.AddError("Email", result);
    #else
      FaultReason faultReason = new FaultReason("Invalid email address.");
      throw new FaultException(faultReason);
    #endif
    }
  }

  #endregion "Private Validation Methods"
}

First, let us take a look at the method InitializeValidationSettings(). This is a partial method, which means that we can choose not to implement it if we have no custom validation logic. If we have property level custom validation like the User entity class above, the method InitializeValidationSettings() is implemented and should include two major functions:

  • We need to decide whether to set the property ValidateEntityOnPropertyChanged to true or false. This property is set to true if there is any entity level custom validation that needs to be checked every time a property gets changed. For the User class, we can simply set it to false because we do not have any entity level validation functions.
  • Also, within this partial method, we need to add all our custom validation functions with the method AddPropertyValidationAction(). The first parameter of this method is a string value which is the name of the property that needs to be validated, and the second parameter points to a custom validation method. In the code sample above, the custom validation method is ValidateEmail().

This custom validation method ValidateEmail() takes only one parameter, which is the value of the property that needs to be validated. If validation fails, the method behaves differently depending on whether being called from the client or server side.

If this method is being called from the client side, a new ValidationResult object with an appropriate error message will be created followed by a call to the method AddError(). This AddError() method will trigger the Silverlight user interface to highlight the properties that failed validation, with the corresponding error messages.

But, if the method is being called from the server side, a FaultException with error message will be thrown. This exception will be passed back to the client side where the user gets notified about what is wrong.

So far, we have finished our discussion on how to add validation attributes, and we are going to talk about adding operation level validation next.

How to Add Operation Level Validation

Compared with adding validation attributes, adding operation level validation is relatively simple. There are only two methods: TryValidate() and Validate().

Adding Validation on Client Side

The TryValidate() method is the one for operation level validation on the client side, and it has two overload forms. The first one takes no parameter. It loops through all data annotation attributes and all custom validation actions. If any validation fails, the function returns false; otherwise it returns true. The other overload takes a string value parameter which is the property name that needs to be validated, and the method only validates against that property specified.

private void OnLoginCommand(User g)
{
  if (!this._authenticationModel.IsBusy)
  {
    // clear any previous error message
    this.LoginScreenErrorMessage = null;
    // only call SignInAsync when all input are validated
    if (g.TryValidate("Name") && g.TryValidate("Password"))
        this._authenticationModel.SignInAsync(g.Name, g.Password);
  }
}

The code sample above is from the class LoginFormViewModel, and it shows that we verify two properties Name and Password every time we make a call to SignInAsync(g.Name, g.Password). Similarly, the code sample below is from the class MyProfileViewModel, and we verify CurrentUser by calling TryValidate() every time we make a call to save any changes:

private void OnSubmitChangeCommand()
{
  try
  {
    if (!_issueVisionModel.IsBusy)
    {
      if (this.CurrentUser != null)
      {
        if (this.CurrentUser.TryValidate())
        {
          // change is not from User Maintenance screen
          this.CurrentUser.IsUserMaintenance = (byte)0;
          this._issueVisionModel.SaveChangesAsync();
        }
      }
    }
  }
  catch (Exception ex)
  {
    // notify user if there is any error
    AppMessages.RaiseErrorMessage.Send(ex);
  }
}

Adding Validation on Server Side

On the server side, the only operation level validation method is Validate(). This method loops through all data annotation attributes and all custom validation actions, and if any validation fails, it will throw an exception, which eventually will be passed back and handled on the client side. Here is an example from the PasswordResetService class inside the project IssueVision.ST.Web.

public void ResetPassword(PasswordResetUser user)
{
  // validate the user on the server side first
  user.Validate();

  using (IssueVisionEntities context = new IssueVisionEntities())
  {
    User foundUser = context.Users.FirstOrDefault(n => n.Name == user.Name);

    if (foundUser != null)
    {
      // retrieve password answer hash and salt from database
      string currentPasswordAnswerHash = 
             context.GetPasswordAnswerHash(user.Name).First();
      string currentPasswordAnswerSalt = 
             context.GetPasswordAnswerSalt(user.Name).First();
      // generate password answer hash
      string passwordAnswerHash = 
             HashHelper.ComputeSaltedHash(user.PasswordAnswer,
             currentPasswordAnswerSalt);

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

        // re-generate passwordAnswer hash and passwordAnswer salt
        currentPasswordAnswerSalt = HashHelper.CreateRandomSalt();
        currentPasswordAnswerHash = 
          HashHelper.ComputeSaltedHash(user.PasswordAnswer, 
                                       currentPasswordAnswerSalt);

        // save changes
        context.ExecuteFunction("UpdatePasswordHashAndSalt"
            , new ObjectParameter("Name", user.Name)
            , new ObjectParameter("PasswordHash", currentPasswordHash)
            , new ObjectParameter("PasswordSalt", currentPasswordSalt));
        context.ExecuteFunction("UpdatePasswordAnswerHashAndSalt"
            , new ObjectParameter("Name", user.Name)
            , new ObjectParameter("PasswordAnswerHash", currentPasswordAnswerHash)
            , new ObjectParameter("PasswordAnswerSalt", currentPasswordAnswerSalt));
      }
      else
        throw new UnauthorizedAccessException(
                      ErrorResources.PasswordQuestionDoesNotMatch);
    }
    else
      throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
  }
}

The operation level validation repeated on the server side does exactly the same thing as what is done on the client side. So, it should never throw an exception. The only reason we add this extra step is because the server side is exposed as WCF Services. We are assuming that a call can come from anywhere, and therefore needs to be validated on server side too.

So far, we have finished our discussion on property level validation, entity level validation, as well as operation level validation. This should be sufficient if you are only interested in how to add validation logic with our current data validation infrastructure. But, if you are still interested in how this infrastructure is implemented, we will cover that next.

Data Validation Infrastructure

The data validation infrastructure mainly consists of three sections of code generated by two T4 templates: IssueVisionClientModel.tt and IssueVisionModel.tt. The client side T4 template IssueVisionClientModel.tt generates an implementation of the INotifyDataErrorInfo interface as well as all its related helper methods. The other T4 template, IssueVisionModel.tt, generates a set of validation helper methods, including the methods TryValidate() and Validate() which we have just seen above. Unlike the code generated by IssueVisionClientModel.tt which is only available on the client side, the code generated by IssueVisionModel.tt is available on both client and server sides.

INotifyDataErrorInfo Interface

As quoted from MSDN documentation, the interface INotifyDataErrorInfo "enables data entity classes to implement custom validation rules and expose validation results to the user interface. You typically implement this interface to provide asynchronous validation logic such as server-side validation. This interface also supports custom error objects, multiple errors per property, cross-property errors, and entity-level errors." It consists of a property HasErrors, a method GerErrors(), and an event ErrorsChanged, and its definition is listed below:

namespace System.ComponentModel
{
  // Defines members that data entity classes can implement to provide custom,
  // asynchronous validation support.
  public interface INotifyDataErrorInfo
  {
    // Gets a value that indicates whether the object has validation errors.
    bool HasErrors { get; }

    // Gets the validation errors for a specified property or for the entire object.
    IEnumerable GetErrors(string propertyName); 

    // Occurs when the validation errors have changed for a property or for the
    // entire object.
    event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
  }

  // Provides data for the INotifyDataErrorInfo.ErrorsChanged event. 
  public sealed class DataErrorsChangedEventArgs : EventArgs
  {
    // Initializes a new instance of the
    // System.ComponentModel.DataErrorsChangedEventArgs class. 
    public DataErrorsChangedEventArgs(string propertyName);

    // Gets the name of the property for which
    // the errors changed, or null or System.String.Empty
    // if the errors affect multiple properties.
    public string PropertyName { get; }
  }
}

If you need a detailed description about the interface INotifyDataErrorInfo, the whitepaper "Implementing Data Validation in Silverlight with INotifyDataErrorInfo" is a good source of reference. Here, I will only briefly go over how it is implemented in our sample.

To implement INotifyDataErrorInfo, we use a property that is a generic Dictionary object called ValidationErrors. This object represents a collection of property names (as keys) and their corresponding lists of ValidationResult objects.

protected Dictionary<string, List<ValidationResult>> ValidationErrors
{
  get
  {
    if (_validationErrors == null)
    {
      _validationErrors = new Dictionary<string, List<ValidationResult>>();
    }
    return _validationErrors;
  }
}

private Dictionary<string, List<ValidationResult>> _validationErrors;

With the help of this protected property, the rest of the code is quite easy to understand: the property HasErrors returns whether ValidationErrors contains an element or not, and the method GetErrors() returns the corresponding list of ValidationResults that match the passed-in property name.

/// <summary>
/// Gets a value indicating whether there are known errors or not.
/// </summary>
bool INotifyDataErrorInfo.HasErrors
{
  get
  {
    return this.ValidationErrors.Keys.Count != 0;
  }
}

/// <summary>
/// Gets the currently known errors
/// for the provided property name. Use String.Empty/null
/// to retrieve entity-level errors.
/// </summary>
IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName)
{
  if (propertyName == null)
  {
    propertyName = string.Empty;
  }

  if (this.ValidationErrors.ContainsKey(propertyName))
  {
    return this.ValidationErrors[propertyName];
  }
  return null;
}

INotifyDataErrorInfo Helper Method

While INotifyDataErrorInfo enables data entity classes to implement custom validation rules and expose validation results to the user interface, it does not know how to communicate with the rest of the sample application. This is why we also created a set of helper methods through the T4 template IssueVisionClientModel.tt. Here is the list of helper methods:

  • AddError(string propertyName, ValidationResult validationResult): adds a new error into the generic Dictionary object ValidationErrors for the property name provided, or for the entity if the property name is String.Empty/null. Every time a new error is added, the method also triggers the ErrorsChanged event so that the user interface gets notified of the change.
  • ClearErrors(string propertyName): removes errors from the generic Dictionary object ValidationErrors for the property name provided, or for the entity if the property name is String.Empty/null. This method also notifies the user interface through the ErrorsChanged event whenever there is any change to the ValidationErrors object.
  • ClearErrors(): removes errors from the generic Dictionary object ValidationErrors for all property names.
  • ValidateEntityOnPropertyChanged: indicates whether or not top-level validation rules must be applied whenever an entity property changes. This property is usually set in the InitializeValidationSettings() method as we discussed above.
  • PropertySetterEntry(string propertyName): removes any known errors for the provided property name by calling ClearErrors().
  • PropertySetterExit(string propertyName, object propertyValue): validates for any known errors for the provided property name.

Please note that the last two methods, PropertySetterEntry() and PropertySetterExit(), are partial methods. They are only implemented on the client side, not on the server side. For any primitive property of a self-tracking entity class, these two methods are called immediately before and after setting a new value, as shown below:

[DataMember]
[Required()]
public string Value
{
  get { return _value; }
  set
  {
    if (_value != value)
    {
      ChangeTracker.RecordOriginalValue("Value", _value);
      PropertySetterEntry("Value");
      _value = value;
      PropertySetterExit("Value", value);
      OnPropertyChanged("Value");
    }
  }
}
private string _value;

Whenever the Value property is set on the client side, a call to PropertySetterEntry("Value") will clear any known errors. After assigning a new value to the property Value, PropertySetterExit("Value", value) will trigger the data validation logic. On the other hand, if the property Value is set on the server side, no data validation is performed as both PropertySetterEntry() and PropertySetterExit() are not implemented.

Validation and Helper Method

Different from the INotifyDataErrorInfo interface implementation and the related helper methods, the set of validation and helper methods generated by the T4 template IssueVisionModel.tt are available on both client and server sides. We can group the properties and methods into the following list:

  • base validation method Validate(string propertyName, object value)
  • client side method TryValidate() and server side method Validate()
  • partial method definitions PropertySetterEntry(string propertyName), PropertySetterExit(string propertyName, object propertyValue), and InitializeValidationSettings()
  • property ValidationActions and method AddPropertyValidationAction(string propertyName, Action<object> validationAction)

The base validation method Validate(string propertyName, object value) loops through all related data annotation attributes as well as all related custom validation actions for the specified property name. If propertyName is String.Empty/null, the validation is on the entity level. This method is mainly used by the client side partial method PropertySetterExit() as follows:

/// <summary>
/// Validate for any known errors for the provided property name
/// </summary>
/// <param name="propertyName">Propery name
///           or String.Empty/null for top-level errors</param>
/// <param name="propertyValue">Property value</param>
partial void PropertySetterExit(string propertyName, object propertyValue)
{
  if (IsDeserializing)
  {
    return;
  }

  if (this.ValidateEntityOnPropertyChanged)
  {
    this.Validate(string.Empty, this);
  }
  else
  {
    this.Validate(propertyName, propertyValue);
  }
}

For the client side operation level validation method TryValidate() and the server side operation level validation method Validate(), we have already covered the usage above, and we will move on to the next in the list, which is the declaration of three partial methods. The partial methods PropertySetterEntry() and PropertySetterExit() are only implemented on client side, and always used as a pair inside the primitive property setter definitions of the entity classes. The other partial method InitializeValidationSettings() is also implemented on client side only, and its implementation is required only if we have custom validation logic for that entity class.

The protected property ValidationActions is a generic Dictionary object that keeps a collection of property names (as keys) and their corresponding lists of Action<object> objects. And, AddPropertyValidationAction(string propertyName, Action<object> validationAction) is a helper method that adds any custom validation action with their matching property name into the property ValidationActions. Let us review the code snippet below from the Issue class to see how the methods InitializeValidationSettings() and AddPropertyValidationAction() are used together.

/// <summary>
/// Partial method to add all validation actions
/// defined for this class
/// </summary>
partial void InitializeValidationSettings()
{
#if SILVERLIGHT
  // there are top-level validation actions defined for this class
  // so, we need to enable this property.
  this.ValidateEntityOnPropertyChanged = true;
#endif
  AddPropertyValidationAction(string.Empty, ValidateStatusResolved);
}

Next Steps

We have finished our discussion about how to add data validation using our enhanced self-tracking entity generator as well as the topic of data validation infrastructure implementation. In our last part, we will move on to cover authentication and authorization using WIF and other remaining topics of interest. I hope you find this article useful, and please rate and/or leave feedback below. Thank you!

History

  • February 16, 2011 - Initial release.
  • March, 2011 - Update to fix multiple bugs including memory leak issues.

License

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

Share

About the Author

Weidong Shen
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

 
GeneralGreat solution, integrating everything together but... PinmemberTomer Shamam11-May-11 6:36 
GeneralRe: Great solution, integrating everything together but... PinmemberWeidong Shen11-May-11 8:32 
GeneralRe: Great solution, integrating everything together but... PinmemberTomer Shamam11-May-11 9:00 
GeneralNo mapping specified for the following EntitySet/AssociationSet - Users, PasswordResetUsers [modified] Pinmemberrsokolowski7-Mar-11 15:12 
GeneralRe: No mapping specified for the following EntitySet/AssociationSet - Users, PasswordResetUsers PinmemberWeidong Shen8-Mar-11 3:52 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140916.1 | Last Updated 8 Mar 2011
Article Copyright 2011 by Weidong Shen
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid