пятница, 27 августа 2010 г.

Silvelight 4 + Ria Services

Недавно на работе мы начали использовать 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);

}

}

}

Комментариев нет:

Отправить комментарий