Теперь нам нужно правильно реализовать логику работы DataForm для пользователей. Можно выдвинуть следующие требования.
· Для изменения пароля необходимо заполнить два поля: пароль и подтверждение пароля. Текущий пароль указывать не обязательно, поскольку эта страница доступна только для администраторов, а они должны всегда иметь возможность изменить пароль.
· Поля с паролем и подтверждением пароля должны быть пустыми пока администратор сам не заполнит их.
· (Соответственно пароль не должен обнуляться, когда он не заполнен, а изменялись только другие поля)
· Логин пользователя должен быть уникальным.
Сейчас код нашей страницы пользователей выглядит примерно так:
Users.xaml
<navigation:Page
x:Class="MyApplication.Users"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
Style="{StaticResource PageStyle}" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices" xmlns:my="clr-namespace:MyApplication.Web.Services" xmlns:my1="clr-namespace:MyApplication.BLL" xmlns:my2="clr-namespace:MyApplication.Controls">
<UserControl.Resources>
Описание стиля для DataGrid (в предыдущей статье)
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
<StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">
<TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
Text="{Binding Path=ApplicationStrings.UsersPageTitle, Source={StaticResource ResourceWrapper}}"/>
<StackPanel>
<my2:BusyIndicator IsBusy="{Binding ElementName=userDomainDataSource, Path=IsBusy}">
<sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding ElementName=userDomainDataSource, Path=Data}" Name="userDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" LoadingRow="userDataGrid_LoadingRow" IsReadOnly="True">
<sdk:DataGrid.Columns>
<sdk:DataGridTemplateColumn Width="80" HeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="chk"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalAlignment="Center"></CheckBox>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTextColumn x:Name="loginColumn" Binding="{Binding Path=Login}" Header="Login" Width="SizeToHeader" />
<sdk:DataGridTextColumn x:Name="nameColumn" Binding="{Binding Path=Name}" Header="Name" Width="SizeToHeader" />
</sdk:DataGrid.Columns>
</sdk:DataGrid>
</my2:BusyIndicator>
<Button Content="Remove selected" Height="23" Name="btnRemove" Width="120" HorizontalAlignment="Left" Margin="10,10,0,0" Click="btnRemove_Click" />
<my2:CustomDataForm x:Name="userDataForm" Margin="0,10,0,0" CurrentItem="{Binding ElementName=userDataGrid, Path=SelectedItem, Mode=TwoWay}"
CommandButtonsVisibility="All" AutoEdit="False" AutoCommit="True" ItemsSource="{Binding ElementName=userDomainDataSource, Path=Data}" EditEnded="userDataForm_EditEnded"/>
</StackPanel>
<riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my1:User, CreateList=true}" Height="0" LoadedData="userDomainDataSource_LoadedData" Name="userDomainDataSource" QueryName="GetUsersQuery" Width="0" >
<riaControls:DomainDataSource.DomainContext>
<my:MyApplicationDomainContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
</StackPanel>
</ScrollViewer>
</Grid>
</navigation:Page>
Модифицируем наш класс с метаданными:
Как уже упомяналось, поле с паролем не должно быть заполнено. Для этого нужно пометить его аттрибутом Exclude.
[Exclude]
public string Password { get; set; }
Нам понадобится дополнительное поле с подтверждением пароля, с другой стороны оно никак не должно быть связано с полями базы данных.
Также отметим сейчас, что мы как обычно будем сохранять в базу данных не просто текст пароля, а его хэш-функцию. Получается, что нам нужно дополнительное поле NewPassword, котороые мы позже будем использовать для сохранения пароля в базу данных.
Добавим новые поля наш класс User
[DataMember]
[CustomValidation(typeof(CompareValidator), "ValidatePassword")]
[Display(Order = 3, Name = "Password Confirmation")]
public string PasswordConfirmation { get; set; }
[DataMember]
[Display(Order = 2, Name = "New Password")]
public string NewPassword { get; set; }
Тут мы добавили новый аттрибут CustomValidation, он нужен нам для сравнения паролей.
CompareValidator.shared.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
namespace MyApplication.BLL
{
public class CompareValidator
{
public static ValidationResult ValidatePassword(string password, ValidationContext context)
{
var user = context.ObjectInstance as User;
if (string.IsNullOrWhiteSpace(password) && string.IsNullOrWhiteSpace(user.NewPassword))
return null;
if (user.NewPassword == password)
return null;
return new ValidationResult("Passwords do not match", new string[] { "PasswordConfirmation" });
}
}
}
Также нам нужен метод для проверки логина пользователя. Напомню, что мы хотели сделать логины уникальными в рамках всего приложения.
На клиентской стороне нам не известны пароли всех остальных пользователей, так что нужно сделать запрос к доменному сервису.
Следующий метод осуществляет асинхронную валидацию логина поьзователя:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
#if SILVERLIGHT
using System.ServiceModel.DomainServices.Client;
using MyApplication.Web.Services;
#endif
namespace MyApplication.BLL
{
public class CompareValidator
{
public static ValidationResult IsLoginAvailable(string login, ValidationContext context)
{
var user = context.ObjectInstance as User;
#if SILVERLIGHT
MyApplicationDomainContext ctx = new MyApplicationDomainContext();
InvokeOperation<bool> availability = ctx.IsLoginAvailable(login, user.ID);
availability.Completed += (s, e) =>
{
if (!availability.HasError && !availability.Value)
{
user.ValidationErrors.Add(new ValidationResult("Username already taken", new string[] { context.MemberName }));
}
};
#endif
return ValidationResult.Success;
}
public static ValidationResult ValidatePassword(string password, ValidationContext context)
{
var user = context.ObjectInstance as User;
if (string.IsNullOrWhiteSpace(password) && string.IsNullOrWhiteSpace(user.NewPassword))
return null;
if (user.NewPassword == password)
return null;
return new ValidationResult("Passwords do not match", new string[] { "PasswordConfirmation" });
}
}
}
Также нам нужно добавить новый метод в сервис:
public IQueryable<User> GetUsers()
{
return this.ObjectContext.Users;
}
public User GetUserById(int id)
{
return this.ObjectContext.Users.FirstOrDefault(p => p.ID == id);
}
public User GetUserByLogin(string login)
{
return this.ObjectContext.Users.FirstOrDefault(p => p.Login == login);
}
public bool IsLoginAvailable(string login, int id)
{
var user = this.ObjectContext.Users.Where(p => p.Login == login && p.ID != id).FirstOrDefault();
return user == null;
}
public void InsertUser(User user)
{
user.Password = Hasher.GetMd5Hash(user.NewPassword);
if ((user.EntityState != EntityState.Detached))
{
this.ObjectContext.ObjectStateManager.ChangeObjectState(user, EntityState.Added);
}
else
{
this.ObjectContext.Users.AddObject(user);
}
}
public void UpdateUser(User currentUser)
{
if (!string.IsNullOrWhiteSpace(currentUser.NewPassword))
{
currentUser.Password = Hasher.GetMd5Hash(currentUser.NewPassword);
}
this.ObjectContext.Users.AttachAsModified(currentUser, this.ChangeSet.GetOriginal(currentUser));
}
public void DeleteUser(User user)
{
if ((user.EntityState == EntityState.Detached))
{
this.ObjectContext.Users.Attach(user);
}
this.ObjectContext.Users.DeleteObject(user);
}
Также нужно обратить внимание на метод UpdateUser. Он использует поле NewPassword для определения того, что ввел пользователь в поле с паролем.
Нужно проверить, не пустое ли это поле, а если это не так, то нужно получить хэш-функцию от этого текста и сохранить ее в базу данных.
public void UpdateUser(User currentUser)
{
if (!string.IsNullOrWhiteSpace(currentUser.NewPassword))
{
currentUser.Password = Hasher.GetMd5Hash(currentUser.NewPassword);
}
this.ObjectContext.Users.AttachAsModified(currentUser, this.ChangeSet.GetOriginal(currentUser));
}
Класс Hasher у меня выглядит вот так:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
namespace MyApplication.BLL
{
public class Hasher
{
public static string GetMd5Hash(string input)
{
MD5 md5Hasher = MD5.Create();
byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
public static bool VerifyMd5Hash(string input, string hash)
{
string hashOfInput = GetMd5Hash(input);
StringComparer comparer = StringComparer.OrdinalIgnoreCase;
if (comparer.Compare(hashOfInput, hash) == 0)
{
return true;
}
else
{
return false;
}
}
}
}
Теперь полный код метаданных для пользователя будет выглядеть так:
// The MetadataTypeAttribute identifies UserMetadata as the class
// that carries additional metadata for the User class.
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
// This class allows you to attach custom attributes to properties
// of the User class.
//
// For example, the following marks the Xyz property as a
// required property and specifies the format for valid values:
// [Required]
// [RegularExpression("[A-Z][A-Za-z0-9]*")]
// [StringLength(32)]
// public string Xyz { get; set; }
internal sealed class UserMetadata
{
// Metadata classes are not meant to be instantiated.
private UserMetadata()
{
}
[Display(AutoGenerateField = false)]
public int ID { get; set; }
[Display(AutoGenerateField = false)]
public EntityCollection<Loan> Loans { get; set; }
[Display(Order = 1)]
[CustomValidation(typeof(CompareValidator), "IsLoginAvailable")]
public string Login { get; set; }
[Display(Order = 0)]
public string Name { get; set; }
[Exclude]
public string Password { get; set; }
[Display(AutoGenerateField = false)]
public EntityCollection<UserProject> UserProjects { get; set; }
}
[DataMember]
[CustomValidation(typeof(CompareValidator), "ValidatePassword")]
[Display(Order = 3, Name = "Password Confirmation")]
public string PasswordConfirmation { get; set; }
[DataMember]
[Display(Order = 2, Name = "New Password")]
public string NewPassword { get; set; }
[Update]
public void Update(User user)
{
}
}
После этих модификаций страница пользователей должна работать правильно, то есть в соответствии с описанными выше требованиями.
Комментариев нет:
Отправить комментарий