一个简单的 Silverlight 4 应用程序(MEF+ MVVM+ WCF RIA Services)第三部分

来源:互联网 发布:淘宝怎么引流到微信 编辑:程序博客网 时间:2024/06/06 09:18

注:本文是Weidong Shen先生在CodeProject上的文章,为了学习方便,进行了全文翻译,后续将以此程序示例为蓝本,写出自已的实用LOB程序来,在此向Weidong Shen先生表示感谢,欢迎大家对我的翻译进行拍砖。题目虽然是简单的Silverlight 4应用程序,其实一点也不简单,还是有很多硬实的内容需要学习的。

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

  • Part 1 - Introduction, Installation, and General Application Design Topics
  • Part 2 - MVVM Light Topics
  • Part 3 - Custom Authentication, Reset Password, and User Maintenance

Contents

  • Introduction简介
  • User, LoginUser, and PasswordResetUser用户,用户登陆与密码重置
  • AuthenticationService授权服务
  • PasswordResetService密码重置服务
  • AuthenticationModel and PasswordResetModel
  • My Profile Screen
  • User Maintenance Screen用户维护页面
  • Further Work进一步的工作
  • References引用
  • History历史

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.
  • 普通用户无权访问用户维护页面,只能更新自己的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 projectIssueVision.Data.Web. The User class is an EntityObject class from theIssueVision Entity Model. Because the User class is defined as a partial class, we can add a few new properties as follows:

User,LoginUser与PasswordResetUser是三个类,定义在IssueVision.Data.Web项目中。User类是来自于IssueVision实体的实体对象类。由于User类定义为部分类,可以向其中添加几个新的属性:

 

/// <summary>///用户类暴露如下数据成员到客户端:/// 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 theUserMetadata class. Specifically, we excluded the properties PasswordAnswerHash,PasswordAnswerSalt, PasswordHash, PasswordSalt, andProfileReset from being auto-generated on the client side. In addition, we have added the new propertiesPassword, NewPassword, PasswordAnswer, and a read-only propertyProfileResetFlag. These changes ensure that any password hash and password salt values only stay on the server side and never transfer through the wire.

上述代码中,通过元数据类为属性添加了许多特性标记。特别地,我们在客户端排除了PasswordAnswerHash, PasswordAnswerSalt,PasswordHash, PasswordSalt, and ProfileReset属性。除此以外,添加了新的属性Password,NewPassword, PasswordAnswer以及只读属性ProfileResetFlag。这些变更确保任何密码的哈希值只保存在服务器端,从而保证了安全。

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

User类由MyProfile与UserMaintenance页面使用,后面我们会重复这个议题。现在看看LoginUser与PasswordResetUser类。

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

LoginUser类是User类的子类,实现了IUser接口。在类AuthenticationService类中使用,如下是定义的代码:

 

/// <summary>/// LoginUser类派生自 User类 ,实现了 IUser接口,/// 该类只暴露如下成员到客户端:/// 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 interfaceIUser, 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.

与User类类似,除了四个属性:Name,Roles,Password与ProfileResetFlage,我们排除了所有客户端自动生成的属性。前两个属性是IUser接口必须的,ProfileResetFlag用于确定帐户新增或更新后是否需要提示用户重置profile。

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

接下来看看PasswordResetUser类。这个类也是User的子类,由PasswordResetService类使用。只暴露四个属性:Name,NewPassword, PasswordQuestion, and PasswordAnswer,定义如下:

 

/// <summary>/// PasswordRestUser类派生自User类/// 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 theAuthenticationService and PasswordResetService classes.

现在我们知道如何定义User,LoginUser与PasswordResetUser类了,接下来看看如何在域服务类中使用它们。

AuthenticationService

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

AuthenticationService是一个域服务类,继承自IAuthentication<LoginUser>,提供自定义授权功能。下面是Login()方法的实现:

 

/// <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))        {           //如IsPersistent设置为true,将保持登陆状态一周           //(或直到登出)            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(), andValidateUser() 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.

Login()方法调用了一个私有方法ValidateUser(),该方法根据用户提供的密码生成一个哈希值,该哈希值与数据库储存的密码哈希值相比较 ,用户授权通过。

PasswordResetService

Similarly, PasswordResetService is also a DomainService class. It has only two functions. The first functionGetUserByName() accepts a user name as the only parameter, and returns back a validPasswordResetUser 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.

类似地,PasswordResetService也是一个域服务类。该类只有两个方法。第一个方法GetUserByName()接受一个用户名作为唯一参数,如果用户名存在于数据库中返回一个合法的PasswordResetUser对象。这一方法由登陆页面调用以便在切换到重置密码页面前找到该用户的安全问题。

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.

另一个方法UpdateUser(),该方法从客户端获取PasswordResetUser对象,检查安全问题与答案是否与数据库中储存的相匹配。如果匹配则新密码就保存到数据库中,保存值为一对密码salt和密码哈希。

 

/// <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 classLoginFormViewModel during runtime, and the ViewModel class has a reference to objects ofAuthenticationModel and PasswordResetModel, which we will discuss now.

在客户端。LoginForm.xaml页面绑定到LoginFormViewModel,而ViewModel类有一个对AuthenticationModel与PasswordResetModel对象的引用,这是我们现在要讨论的问题。

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

AuthenticationModel类基于接口IAutentiactionModel定义,定义如下:

 

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():

下面是主要方法LoginAsync()的实现:

/// <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-sideLogin() function from the AuthenticationService class we discussed above.

Likewise, PasswordResetModel is based on the interface IPasswordResetModel.

LoginAsync()内部的Login()方法最终会调用服务器端的Login()方法(在AuthenticationService类中)。同样,PasswordResetModel基于IPasswordResetModel接口:

 

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 classLoginFormViewModel 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-sideUpdateUser() from the PasswordResetService class to verify and save a new password if both the security question and answer match what is in the database.

在切换到重置密码页面前,GetUerByNameAsync()方法由LoginFormViewModel类调用,以找到正确的安全问题。SaveUserAsync()方法在ResetPasswordCommand内使用,最终会调用服务端的PasswordResetService类中的UpdateUser()方法确认并保存新密码(前提是安全问题与答案与数据库储存的相匹配)。

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 classMyProfileViewModel, which retrieves and updates user information through two server-side functionsGetCurrentUser() and UpdateUser() from the IssueVisionService class.

前已述及,My Profile页面采用User类。该页面绑定到MyProfileViewModel,该类通过IssueVisionService类的两个服务器端方法GetCurrentUser()和UpdateUser()获取并更新用户信息。

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:

当帐户新建或更新后第一次登陆时,将显示My Profile页面而非Home页面:

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

在视图模型中该项功能的真正实现是在MainPageViewModel类中,如下所示:

 

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;        // 如果ProfileResetFlag设置        // 询问用户是否先重置profile        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 classUserMaintenanceViewModel, and eventually retrieves and updates user information through the functionsGetUsers(), InsertUser(), UpdateUser(), andDeleteUser() from the IssueVisionService class on the server side. Let's check how the functionInsertUser() is implemented:

最后讨论一下用户维护页面。该页面只能由管理员访问,绑定到UserMaintenanceViewModel类,最终是通过服务器端的IssueVisionService类的方法GetUsers(),InsertUser(), UpdateUser(), and DeleteUser()获取和更新用户信息。下面的代码是方法InsertUser()的实现:

 

public void InsertUser(User user){    // 检查插入用户权限
    if (CheckUserInsertPermission(user) && user.IsUserMaintenance)    {        // 验证用户是否已经存在        User foundUser = ObjectContext.Users.Where(            n => n.Name == user.Name).FirstOrDefault();        if (foundUser != null)            throw new ValidationException(ErrorResources.CannotInsertDuplicateUser);        // 重新生成密码哈希与Salt        user.PasswordSalt = HashHelper.CreateRandomSalt();        user.PasswordHash = HashHelper.ComputeSaltedHash(                             user.NewPassword, user.PasswordSalt);        // 设置有效的安全问题        SecurityQuestion securityQuestion =           ObjectContext.SecurityQuestions.FirstOrDefault();        if (securityQuestion != null)            user.PasswordQuestion = securityQuestion.PasswordQuestion;        // 设置无人知晓的密码问题        user.PasswordAnswerSalt = HashHelper.CreateRandomSalt();        user.PasswordAnswerHash = HashHelper.CreateRandomSalt();        // 要求用户重置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.

由此可见,创建新用户时实际上并没有设置安全问题,这民是需要提醒用户首次登陆后设置profile的原因、

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

  • Building Line of Business Applications with Microsoft Silverlight 4
  • Architecting Silverlight 4 with RIA Services, MEF, and MVVM
  • MVVM Light Toolkit
  • Silverlight Toolkit
  • Silverlight Menu

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 underThe Code Project Open License (CPOL)

About the Author

Weidong Shen

Software Developer (Senior)
United States United States
Member

Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

原创粉丝点击