Недавно на работе мы начали использовать Silverlight 4 вместе с WCF Ria Services. В этой и следующих статьях я хочу описать несколько, как мне кажется, полезных вещей об этих технологиях.
Вообще Silvelight 4 + Ria Services позволяет достаточно быстро создавать приложения, ориентированные на данные, при этом получается клиентское приложение, что, пожалуй, более приятно для многих программистов, чем веб-приложение.
С другой стороны обучающие материалы, которые можно в интернете практически не применимы для приложений из «реального мира». Вообще Silverlight контролы практически не кастомизируемы, а их поведение очень сложно изменить под свои потребности. Хотя, может это все только мое мнение.
Решения множества проблем обычно можно найти на форумах и в личных блогах. Я хочу собрать в следующий статьях некоторые моменты, которые мне приходось искать во время работы. Надеюсь, что мои простые советы смогут помочь кому-то при разработке Silverlight приложений.
Перед тем как переходить к рассмотрению более сложных вещей, создадим новый Silverlight проект.
После установки всех необходимых компонентов для Visual Studio 2010 (Silverlight 4 SDK, Silverlight 4 Tools for Visual Studio 2010 и т.д.) должен появиться новый вид проекта – Silverlight Business Application.
Полученное решение состоит из двух проектов:
Silvelight проект – BusinessApplication1
ASP.NET проект- BusinessApplication1.Web
Посмотрим содержимое этих проектов:
Веб-проект содержит код необходимый для регистрации новых пользователей и аутентификации уже созданных.
Вся система аутентификации будет работать поверх стандартных средств ASP.NET через веб-сервис AuthenticationService, описанный в одноименном файле. Пока можно использовать мтандартные методы как заглушку, в позже, скорее всего, придется подключить собственный поставщик данных о пользователях.
В дальнейшем в нашем приложении будет использоваться информация о пользователях, которая хранится в таблице в базе данных SQL Server.
Регистрация пользователей происходит через сервис RegistrationService. В нем много кода, так что рассмотрим его подробнее.
namespace BusinessApplication1.Web
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using System.Web.Profile;
using System.Web.Security;
using BusinessApplication1.Web.Resources;
/// <summary>
/// RIA Services Domain Service that exposes methods for performing user
/// registrations.
/// </summary>
[EnableClientAccess]
public class UserRegistrationService : DomainService
{
/// <summary>
/// Role to which users will be added by default.
/// </summary>
public const string DefaultRole = "Registered Users";
//// NOTE: This is a sample code to get your application started. In the production code you would
//// want to provide a mitigation against a denial of service attack by providing CAPTCHA
//// control functionality or verifying user's email address.
/// <summary>
/// Adds a new user with the supplied <see cref="RegistrationData"/> and <paramref name="password"/>.
/// </summary>
/// <param name="user">The registration information for this user.</param>
/// <param name="password">The password for the new user.</param>
[Invoke(HasSideEffects = true)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
public CreateUserStatus CreateUser(RegistrationData user,
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[RegularExpression("^.*[^a-zA-Z0-9].*$", ErrorMessageResourceName = "ValidationErrorBadPasswordStrength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[StringLength(50, MinimumLength = 7, ErrorMessageResourceName = "ValidationErrorBadPasswordLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
string password)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
// Run this BEFORE creating the user to make sure roles are enabled and the default role
// will be available
//
// If there are a problem with the role manager it is better to fail now than to have it
// happening after the user is created
if (!Roles.RoleExists(UserRegistrationService.DefaultRole))
{
Roles.CreateRole(UserRegistrationService.DefaultRole);
}
// NOTE: ASP.NET by default uses SQL Server Express to create the user database.
// CreateUser will fail if you do not have SQL Server Express installed.
MembershipCreateStatus createStatus;
Membership.CreateUser(user.UserName, password, user.Email, user.Question, user.Answer, true, null, out createStatus);
if (createStatus != MembershipCreateStatus.Success)
{
return UserRegistrationService.ConvertStatus(createStatus);
}
// Assign it to the default role
// This *can* fail but only if role management is disabled
Roles.AddUserToRole(user.UserName, UserRegistrationService.DefaultRole);
// Set its friendly name (profile setting)
// This *can* fail but only if Web.config is configured incorrectly
ProfileBase profile = ProfileBase.Create(user.UserName, true);
profile.SetPropertyValue("FriendlyName", user.FriendlyName);
profile.Save();
return CreateUserStatus.Success;
}
/// <summary>
/// Query method that exposes the <see cref="RegistrationData"/> class to Silverlight client code.
/// </summary>
/// <remarks>
/// This query method is not used and will throw <see cref="NotSupportedException"/> if called.
/// Its primary job is to indicate the <see cref="RegistrationData"/> class should be made
/// available to the Silverlight client.
/// </remarks>
/// <returns>Not applicable.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
public IEnumerable<RegistrationData> GetUsers()
{
throw new NotSupportedException();
}
private static CreateUserStatus ConvertStatus(MembershipCreateStatus createStatus)
{
switch (createStatus)
{
case MembershipCreateStatus.Success: return CreateUserStatus.Success;
case MembershipCreateStatus.InvalidUserName: return CreateUserStatus.InvalidUserName;
case MembershipCreateStatus.InvalidPassword: return CreateUserStatus.InvalidPassword;
case MembershipCreateStatus.InvalidQuestion: return CreateUserStatus.InvalidQuestion;
case MembershipCreateStatus.InvalidAnswer: return CreateUserStatus.InvalidAnswer;
case MembershipCreateStatus.InvalidEmail: return CreateUserStatus.InvalidEmail;
case MembershipCreateStatus.DuplicateUserName: return CreateUserStatus.DuplicateUserName;
case MembershipCreateStatus.DuplicateEmail: return CreateUserStatus.DuplicateEmail;
default: return CreateUserStatus.Failure;
}
}
}
/// <summary>
/// An enumeration of the values that can be returned from <see cref="UserRegistrationService.CreateUser"/>
/// </summary>
public enum CreateUserStatus
{
Success = 0,
InvalidUserName = 1,
InvalidPassword = 2,
InvalidQuestion = 3,
InvalidAnswer = 4,
InvalidEmail = 5,
DuplicateUserName = 6,
DuplicateEmail = 7,
Failure = 8,
}
}
По сути, тут есть только один метод – CreateUser, который берет на себя ответственность за валидацию пользовательских данных. На вход он принимает экземпляр типа RegistrationData, который также описывается в созданном проекте.
namespace BusinessApplication1.Web
{
using System.ComponentModel.DataAnnotations;
using BusinessApplication1.Web.Resources;
/// <summary>
/// Class containing the values and validation rules for user registration.
/// </summary>
public sealed partial class RegistrationData
{
/// <summary>
/// Gets and sets the user name.
/// </summary>
[Key]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[Display(Order = 0, Name = "UserNameLabel", ResourceType = typeof(RegistrationDataResources))]
[RegularExpression("^[a-zA-Z0-9_]*$", ErrorMessageResourceName = "ValidationErrorInvalidUserName", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[StringLength(255, MinimumLength = 4, ErrorMessageResourceName = "ValidationErrorBadUserNameLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
public string UserName { get; set; }
/// <summary>
/// Gets and sets the email address.
/// </summary>
[Key]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[Display(Order = 2, Name = "EmailLabel", ResourceType = typeof(RegistrationDataResources))]
[RegularExpression(@"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$",
ErrorMessageResourceName = "ValidationErrorInvalidEmail", ErrorMessageResourceType = typeof(ValidationErrorResources))]
public string Email { get; set; }
/// <summary>
/// Gets and sets the friendly name of the user.
/// </summary>
[Display(Order = 1, Name = "FriendlyNameLabel", Description = "FriendlyNameDescription", ResourceType = typeof(RegistrationDataResources))]
[StringLength(255, MinimumLength = 0, ErrorMessageResourceName = "ValidationErrorBadFriendlyNameLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
public string FriendlyName { get; set; }
/// <summary>
/// Gets and sets the security question.
/// </summary>
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[Display(Order = 5, Name = "SecurityQuestionLabel", ResourceType = typeof(RegistrationDataResources))]
public string Question { get; set; }
/// <summary>
/// Gets and sets the answer to the security question.
/// </summary>
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[Display(Order = 6, Name = "SecurityAnswerLabel", ResourceType = typeof(RegistrationDataResources))]
public string Answer { get; set; }
}
}
В ходе работы мне не пришлось делать регистрацию новых пользователей при помощи этого сервиса, так что ничего умного написать о нем не могу.
На стороне Silverlight приложения существует класс, который расширяет RegistrationData.
namespace BusinessApplication1.Web
{
using System;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel.DomainServices.Client;
using System.ServiceModel.DomainServices.Client.ApplicationServices;
using BusinessApplication1.Web.Resources;
/// <summary>
/// Extensions to provide client side custom validation and data binding to <see cref="RegistrationData"/>.
/// </summary>
public partial class RegistrationData
{
private OperationBase currentOperation;
/// <summary>
/// Gets or sets a function that returns the password.
/// </summary>
internal Func<string> PasswordAccessor { get; set; }
/// <summary>
/// Gets and sets the password.
/// </summary>
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[Display(Order = 3, Name = "PasswordLabel", Description = "PasswordDescription", ResourceType = typeof(RegistrationDataResources))]
[RegularExpression("^.*[^a-zA-Z0-9].*$", ErrorMessageResourceName = "ValidationErrorBadPasswordStrength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[StringLength(50, MinimumLength = 7, ErrorMessageResourceName = "ValidationErrorBadPasswordLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
public string Password
{
get
{
return (this.PasswordAccessor == null) ? string.Empty : this.PasswordAccessor();
}
set
{
this.ValidateProperty("Password", value);
this.CheckPasswordConfirmation();
// Do not store the password in a private field as it should
// not be stored in memory in plain-text. Instead, the supplied
// PasswordAccessor serves as the backing store for the value.
this.RaisePropertyChanged("Password");
}
}
/// <summary>
/// Gets or sets a function that returns the password confirmation.
/// </summary>
internal Func<string> PasswordConfirmationAccessor { get; set; }
/// <summary>
/// Gets and sets the password confirmation string.
/// </summary>
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
[Display(Order = 4, Name = "PasswordConfirmationLabel", ResourceType = typeof(RegistrationDataResources))]
public string PasswordConfirmation
{
get
{
return (this.PasswordConfirmationAccessor == null) ? string.Empty : this.PasswordConfirmationAccessor();
}
set
{
this.ValidateProperty("PasswordConfirmation", value);
this.CheckPasswordConfirmation();
// Do not store the password in a private field as it should
// not be stored in memory in plain-text. Instead, the supplied
// PasswordAccessor serves as the backing store for the value.
this.RaisePropertyChanged("PasswordConfirmation");
}
}
/// <summary>
/// Gets or sets the current registration or login operation.
/// </summary>
internal OperationBase CurrentOperation
{
get
{
return this.currentOperation;
}
set
{
if (this.currentOperation != value)
{
if (this.currentOperation != null)
{
this.currentOperation.Completed -= (s, e) => this.CurrentOperationChanged();
}
this.currentOperation = value;
if (this.currentOperation != null)
{
this.currentOperation.Completed += (s, e) => this.CurrentOperationChanged();
}
this.CurrentOperationChanged();
}
}
}
/// <summary>
/// Gets a value indicating whether the user is presently being registered or logged in.
/// </summary>
[Display(AutoGenerateField = false)]
public bool IsRegistering
{
get
{
return this.CurrentOperation != null && !this.CurrentOperation.IsComplete;
}
}
/// <summary>
/// Helper method for when the current operation changes.
/// Used to raise appropriate property change notifications.
/// </summary>
private void CurrentOperationChanged()
{
this.RaisePropertyChanged("IsRegistering");
}
/// <summary>
/// Checks to ensure the password and confirmation match. If they don't match,
/// then a validation error is added.
/// </summary>
private void CheckPasswordConfirmation()
{
// If either of the passwords has not yet been entered, then don't test for equality between
// the fields. The Required attribute will ensure a value has been entered for both fields.
if (string.IsNullOrWhiteSpace(this.Password)
|| string.IsNullOrWhiteSpace(this.PasswordConfirmation))
{
return;
}
// If the values are different, then add a validation error with both members specified
if (this.Password != this.PasswordConfirmation)
{
this.ValidationErrors.Add(new ValidationResult(ValidationErrorResources.ValidationErrorPasswordConfirmationMismatch, new string[] { "PasswordConfirmation", "Password" }));
}
}
/// <summary>
/// Perform logic after the UserName value has been entered
/// </summary>
/// <param name="userName">The user name value that was entered.</param>
/// <remarks>
/// Allow the form to indicate when the value has been completely entered, because
/// using the OnUserNameChanged method can lead to a premature call before the user
/// has finished entering the value in the form.
/// </remarks>
internal void UserNameEntered(string userName)
{
// Auto-Fill FriendlyName to match UserName for new entities when there is not a friendly name specified
if ((this.EntityState == EntityState.New || this.EntityState == EntityState.Detached)
&& string.IsNullOrWhiteSpace(this.FriendlyName))
{
this.FriendlyName = userName;
}
}
/// <summary>
/// Creates a new <see cref="LoginParameters"/>
/// initialized with this entity's data (IsPersistent will default to false)
/// </summary>
public LoginParameters ToLoginParameters()
{
return new LoginParameters(this.UserName, this.Password, false, null);
}
}
}
Комментариев нет:
Отправить комментарий