Показаны сообщения с ярлыком Authentication. Показать все сообщения
Показаны сообщения с ярлыком Authentication. Показать все сообщения

понедельник, 30 августа 2010 г.

Немного об аутентификации в Silverlight 4 - Часть 4

Теперь нам нужно правильно реализовать логику работы 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)

{

}

}

После этих модификаций страница пользователей должна работать правильно, то есть в соответствии с описанными выше требованиями.

Немного об аутентификации в Silverlight 4 - Часть 3

Сейчас мы разработаем страницу, с которой можно будет добавлять поных пользователей, а также изменяться их параметры, такие как имя, логин и пароль.

Сначала создадим новую страницу Users.

У нас будет DataGrid с пользователями. Заодно мы посмотрим, как можно создать Select All чекбокс в заголовке колонки.

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>

<Style x:Key="DataGridColumnHeaderStyle" TargetType="sdk:DataGridColumnHeader">

<Setter Property="Foreground" Value="#FF000000"/>

<Setter Property="HorizontalContentAlignment" Value="Left"/>

<Setter Property="VerticalContentAlignment" Value="Center"/>

<Setter Property="IsTabStop" Value="False"/>

<Setter Property="SeparatorBrush" Value="#FFC9CACA"/>

<Setter Property="Padding" Value="4"/>

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="sdk:DataGridColumnHeader">

<Grid x:Name="Root">

<Grid.ColumnDefinitions>

<ColumnDefinition/>

<ColumnDefinition Width="Auto"/>

</Grid.ColumnDefinitions>

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates">

<VisualState x:Name="Normal"/>

<VisualState x:Name="MouseOver">

<Storyboard>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundRectangle" Storyboard.TargetProperty="(Fill).Color" To="#FF448DCA"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[3].Color" To="#7FFFFFFF"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[2].Color" To="#CCFFFFFF"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[1].Color" To="#F2FFFFFF"/>

</Storyboard>

</VisualState>

<VisualState x:Name="Pressed">

<Storyboard>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundRectangle" Storyboard.TargetProperty="(Fill).Color" To="#FF448DCA"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[0].Color" To="#D8FFFFFF"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[1].Color" To="#C6FFFFFF"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[2].Color" To="#8CFFFFFF"/>

<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Fill).(GradientStops)[3].Color" To="#3FFFFFFF"/>

</Storyboard>

</VisualState>

</VisualStateGroup>

<VisualStateGroup x:Name="SortStates">

<VisualState x:Name="Unsorted"/>

<VisualState x:Name="SortAscending"/>

<VisualState x:Name="SortDescending"/>

</VisualStateGroup>

</VisualStateManager.VisualStateGroups>

<Rectangle x:Name="BackgroundRectangle" Fill="#FF1F3B53" Stretch="Fill" Grid.ColumnSpan="2"/>

<Rectangle x:Name="BackgroundGradient" Stretch="Fill" Grid.ColumnSpan="2">

<Rectangle.Fill>

<LinearGradientBrush EndPoint=".7,1" StartPoint=".7,0">

<GradientStop Color="#FCFFFFFF" Offset="0.015"/>

<GradientStop Color="#F7FFFFFF" Offset="0.375"/>

<GradientStop Color="#E5FFFFFF" Offset="0.6"/>

<GradientStop Color="#D1FFFFFF" Offset="1"/>

</LinearGradientBrush>

</Rectangle.Fill>

</Rectangle>

<Rectangle x:Name="VerticalSeparator" Fill="{TemplateBinding SeparatorBrush}" VerticalAlignment="Stretch" Width="1" Visibility="{TemplateBinding SeparatorVisibility}" Grid.Column="1"/>

<CheckBox x:Name="chkSelectAll" Click="chk_Click" Margin="2,0,0,0" d:LayoutOverrides="Width, Height" VerticalAlignment="Center" HorizontalAlignment="Center"/>

</Grid>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</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>

Чтобы чекбокс в заголовке работал нужно создать коллекцию чекбоксов и обновлять их состояние при нажатии на чекбокс.

private List<CheckBox> _checkboxes;

private void chk_Click(object sender, System.Windows.RoutedEventArgs e)

{

CheckBox chk = sender as CheckBox;

bool check = chk.IsChecked.Value;

foreach (CheckBox chkbox in _checkboxes)

{

chkbox.IsChecked = check;

}

}

private void userDataGrid_LoadingRow(object sender, DataGridRowEventArgs e)

{

CheckBox chk = userDataGrid.Columns[0].GetCellContent(e.Row) as CheckBox;

chk.IsChecked = false;

if (!_checkboxes.Contains(chk))

{

_checkboxes.Add(chk);

}

}

Другого способа сделать это, похоже, пока нет.

При нажатии на кнопку нужно удалять все записи из таблицы, разумеется, после подтверждения пользователя.

private void btnRemove_Click(object sender, System.Windows.RoutedEventArgs e)

{

if (HtmlPage.Window.Confirm("Are you sure you want to remove selected users?"))

{

var ctx = (userDomainDataSource.DomainContext as MyApplicationDomainContext);

foreach (var item in _checkboxes)

{

if (item.IsChecked == true)

{

var row = DataGridRow.GetRowContainingElement(item);

var data = row.DataContext;

ctx.Users.Remove(data as User);

}

}

ctx.SubmitChanges();

}

}

Страница будет доступна только для администраторов, так что в методе OnNavigateTo проверим, является ли пользователь администратором.

protected override void OnNavigatedTo(NavigationEventArgs e)

{

if (!WebContext.Current.User.IsAuthenticated)

{

this.NavigationService.Navigate(new Uri("/Home", UriKind.Relative));

return;

}

if (!WebContext.Current.User.IsAdmin)

{

NavigationService.Navigate(new Uri("/Home", UriKind.Relative));

}

_checkboxes = new List<CheckBox>();

}

Весь код Users.xaml.cs:

namespace MyApplication

{

using System.Windows.Controls;

using System.Windows.Navigation;

using System;

using System.Collections.Generic;

using System.Windows.Browser;

using MyApplication.Web.Services;

using MyApplication.BLL;

/// <summary>

/// <see cref="Page"/> class to present information about the current application.

/// </summary>

public partial class Users : Page

{

private List<CheckBox> _checkboxes;

/// <summary>

/// Creates a new instance of the <see cref="Users"/> class.

/// </summary>

public Users()

{

if (WebContext.Current.User.IsAuthenticated)

{

InitializeComponent();

}

this.Title = ApplicationStrings.UsersPageTitle;

}

/// <summary>

/// Executes when the user navigates to this page.

/// </summary>

protected override void OnNavigatedTo(NavigationEventArgs e)

{

if (!WebContext.Current.User.IsAuthenticated)

{

this.NavigationService.Navigate(new Uri("/Home", UriKind.Relative));

return;

}

if (!WebContext.Current.User.IsAdmin)

{

NavigationService.Navigate(new Uri("/Home", UriKind.Relative));

}

_checkboxes = new List<CheckBox>();

}

protected override void OnNavigatedFrom(NavigationEventArgs e)

{

base.OnNavigatedFrom(e);

}

private void userDomainDataSource_LoadedData(object sender, LoadedDataEventArgs e)

{

if (e.HasError)

{

System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK);

e.MarkErrorAsHandled();

}

}

private void chk_Click(object sender, System.Windows.RoutedEventArgs e)

{

CheckBox chk = sender as CheckBox;

bool check = chk.IsChecked.Value;

foreach (CheckBox chkbox in _checkboxes)

{

chkbox.IsChecked = check;

}

}

private void userDataGrid_LoadingRow(object sender, DataGridRowEventArgs e)

{

CheckBox chk = userDataGrid.Columns[0].GetCellContent(e.Row) as CheckBox;

chk.IsChecked = false;

if (!_checkboxes.Contains(chk))

{

_checkboxes.Add(chk);

}

}

private void userDataForm_EditEnded(object sender, DataFormEditEndedEventArgs e)

{

userDomainDataSource.DomainContext.SubmitChanges();

}

private void btnRemove_Click(object sender, System.Windows.RoutedEventArgs e)

{

if (HtmlPage.Window.Confirm("Are you sure you want to remove selected users?"))

{

var ctx = (userDomainDataSource.DomainContext as MyApplicationDomainContext);

foreach (var item in _checkboxes)

{

if (item.IsChecked == true)

{

var row = DataGridRow.GetRowContainingElement(item);

var data = row.DataContext;

ctx.Users.Remove(data as User);

}

}

ctx.SubmitChanges();

}

}

}

}

Теперь нужно разобраться с DataForm, но об этом в следующий раз

Немного об аутентификации в Silverlight 4 - Часть 2

Сейчас мы расширим наши классы так, чтобы они использовали не аутентификацию по умолчанию, а аутентификацию, в которой пользователи берутся из базы данных.

Для этого переопределим несколько методов в класса AuthenticationService.

Нам нужно переопределить методы ValidateUser и GetAuthenticatedUser. ValidateUser должен вернуть true, если пользователь ввел правильные login и пароль.

GetAuthenticatedUser должен вернуть объект типа User (тот, который создан автоматически).

AuthenticationService.cs

namespace MyApplication.Web

{

using System.Security.Authentication;

using System.ServiceModel.DomainServices.Hosting;

using System.ServiceModel.DomainServices.Server;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Threading;

using System.Security.Principal;

using HLUser = MyApplication.BLL.User;

using MyApplication.BLL;

using MyApplication.Web.Services;

/// <summary>

/// RIA Services DomainService responsible for authenticating users when

/// they try to log on to the application.

///

/// Most of the functionality is already provided by the base class

/// AuthenticationBase

/// </summary>

[EnableClientAccess]

public class AuthenticationService : AuthenticationBase<User>

{

protected override bool ValidateUser(string username, string password)

{

return false;

}

protected override User GetAuthenticatedUser(IPrincipal pricipal)

{

return null;

}

}

}

Пока мы не может вернуть ничего разумного. Расширим наши классы User. Добавим несколько новых свойств, которые нам пригодятся в дальнейшем.

User.cs

namespace MyApplication.Web

{

using System.Runtime.Serialization;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Collections.Generic;

/// <summary>

/// Class containing information about the authenticated user.

/// </summary>

public partial class User : UserBase

{

//// NOTE: Profile properties can be added for use in Silverlight application.

//// To enable profiles, edit the appropriate section of web.config file.

////

//// public string MyProfileProperty { get; set; }

/// <summary>

/// Gets and sets the friendly name of the user.

/// </summary>

public string FriendlyName { get; set; }

public bool IsAdmin { get; internal set; }

public bool HasProjects { get; internal set; }

public bool HasManagedProjects { get; internal set; }

public int ID { get; set; }

public List<int> ManagedProjects { get; set; }

}

}

User.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace MyApplication.BLL

{

public partial class User

{

public static bool Authenticate(string userName, string password)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null && Hasher.VerifyMd5Hash(password, user.Password))

{

return true;

}

}

return false;

}

public static bool IsUserExist(string userName)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null)

{

return true;

}

}

return false;

}

public static string GetUserName(string userName)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null)

{

return user.Name;

}

}

return null;

}

public static User GetUserByLogin(string userName)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null)

{

return user;

}

}

return null;

}

public int GetProjectsCount()

{

using (var ctx = new MyApplicationEntities())

{

return ctx.UserProjects.Where(p => p.UserID == ID).Count();

}

}

public int GetManagedProjectsCount()

{

using (var ctx = new MyApplicationEntities())

{

return ctx.UserProjects.Include("ProjectRole").Where(p => p.UserID == ID && p.ProjectRole.Name == "Manager").Count();

}

}

public List<int> GetManagedProjects()

{

using (var ctx = new MyApplicationEntities())

{

return (from pu in ctx.UserProjects.Include("ProjectRole")

where pu.UserID == ID && pu.ProjectRole.Name == "Manager"

select pu.ProjectID).ToList();

}

}

public static string GetProjectRole(int userId, int projectId)

{

using (var ctx = new MyApplicationEntities())

{

return (from up in ctx.UserProjects.Include("ProjectRole")

where up.UserID == userId && up.ProjectID == projectId

select up.ProjectRole.Name).FirstOrDefault();

}

}

}

}

User.shared.cs

using MyApplication.BLL;

namespace MyApplication.Web

{

/// <summary>

/// Partial class extending the User type that adds shared properties and methods

/// that will be available both to the server app and the client app

/// </summary>

public partial class User

{

/// <summary>

/// Returns the user display name, which by default is its Friendly Name,

/// and if that is not set, its User Name

/// </summary>

public string DisplayName

{

get

{

if (!string.IsNullOrEmpty(this.FriendlyName))

{

return this.FriendlyName;

}

else

{

return this.Name;

}

}

}

}

}

Теперь, когда нужные свойства добавлены можно переходить к реализации класса AuthenticationService. Стоит обратить внимание на то, что сейчас мы можем не заполнять поля класса User в методе GetAuthenticatedUser. С другой стороны, нужно понимать, что, поскольку Silverlight – клиентская технология, нам придется подгружать все недостающие данные асинхронно во время работы приложения.

Я не хочу этого делать, так что я заполню все поля сразу. Из-за этого появится следующая проблема – данные в полях User будут действительны до момента нового вызова GetAuthenticatedUser, то есть до момента нового входа пользователя в систему.

AuthenticationService.cs

namespace MyApplication.Web

{

using System.Security.Authentication;

using System.ServiceModel.DomainServices.Hosting;

using System.ServiceModel.DomainServices.Server;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Threading;

using System.Security.Principal;

using HLUser = MyApplication.BLL.User;

using MyApplication.BLL;

using MyApplication.Web.Services;

/// <summary>

/// RIA Services DomainService responsible for authenticating users when

/// they try to log on to the application.

///

/// Most of the functionality is already provided by the base class

/// AuthenticationBase

/// </summary>

[EnableClientAccess]

public class AuthenticationService : AuthenticationBase<User>

{

protected override bool ValidateUser(string username, string password)

{

return HLUser.Authenticate(username, password);

}

protected override User GetAuthenticatedUser(IPrincipal pricipal)

{

User user = null;

if (HLUser.IsUserExist(pricipal.Identity.Name))

{

user = new User();

var dbUser = HLUser.GetUserByLogin(pricipal.Identity.Name);

if (dbUser != null)

{

user.Name = dbUser.Name;

user.IsAdmin = dbUser.IsAdmin;

user.HasProjects = dbUser.GetProjectsCount() > 0;

user.HasManagedProjects = dbUser.GetManagedProjectsCount() > 0;

user.ID = dbUser.ID;

user.ManagedProjects = dbUser.GetManagedProjects();

}

}

return user;

}

}

}

Поскольку сейчас мы будем использовать таблицу в базе данных для аутентификации пользователей, стоит отключить возможность регистрации пользователей, которая автоматически доступна при создании нового проекта.

Для этого изменим страницу LoginForm следующим образом:

<StackPanel

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="clr-namespace:MyApplication.Controls"

xmlns:login="clr-namespace:MyApplication.LoginUI"

x:Class="MyApplication.LoginUI.LoginForm"

KeyDown="LoginForm_KeyDown"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

mc:Ignorable="d"

d:DataContext="{d:DesignInstance Type=login:LoginInfo}">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition/>

</Grid.RowDefinitions>

<local:BusyIndicator x:Name="busyIndicator" BusyContent="{Binding Path=ApplicationStrings.BusyIndicatorLoggingIn, Source={StaticResource ResourceWrapper}}"

IsBusy="{Binding IsLoggingIn}">

<StackPanel Orientation="Vertical">

<local:CustomDataForm x:Name="loginForm"

Padding="10,0,10,0"

CurrentItem="{Binding}"

IsEnabled="{Binding IsLoggingIn, Converter={StaticResource NotOperatorValueConverter}}"

AutoEdit="True" CommandButtonsVisibility="None" HeaderVisibility="Collapsed"

AutoGeneratingField="LoginForm_AutoGeneratingField"

Style="{StaticResource LoginDataFormStyle}" />

</StackPanel>

</local:BusyIndicator>

<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">

<Button x:Name="loginButton" Content="{Binding Path=ApplicationStrings.OKButton, Source={StaticResource ResourceWrapper}}" Click="LoginButton_Click" Style="{StaticResource RegisterLoginButtonStyle}" IsEnabled="{Binding Path=CanLogIn}" />

<Button x:Name="loginCancel" Content="{Binding Path=ApplicationStrings.CancelButton, Source={StaticResource ResourceWrapper}}" Click="CancelButton_Click" Style="{StaticResource RegisterLoginButtonStyle}" />

</StackPanel>

<!--<StackPanel Grid.Row="1" Grid.Column="0" Style="{StaticResource RegisterLoginLinkPanelStyle}">

<TextBlock Text="{Binding Path=ApplicationStrings.NotRegisteredYetLabel, Source={StaticResource ResourceWrapper}}" Style="{StaticResource CommentStyle}"/>

<HyperlinkButton x:Name="registerNow" Content="{Binding Path=ApplicationStrings.RegisterNowButton, Source={StaticResource ResourceWrapper}}" Click="RegisterNow_Click" IsEnabled="{Binding IsLoggingIn, Converter={StaticResource NotOperatorValueConverter}}" />

</StackPanel>-->

</Grid>

</StackPanel>

Теперь мы можем использовать все поля, которые были описаны в классе User в нашем Silverlight приложении.

Также имеет смысл подписаться на события LoggedIn и LoggedOut на странице Main, чтобы можно было упралять пользовательским интерфейсом.

Например, администратор может видеть больше страниц, чем все другие пользователи, можно реализовать это следующим образом:

MainPage.xaml.cs

namespace MyApplication

{

using System.Windows;

using System.Windows.Controls;

using System.Windows.Navigation;

using MyApplication.LoginUI;

using System;

/// <summary>

/// <see cref="UserControl"/> class providing the main UI for the application.

/// </summary>

public partial class MainPage : UserControl

{

/// <summary>

/// Creates a new <see cref="MainPage"/> instance.

/// </summary>

public MainPage()

{

InitializeComponent();

this.loginContainer.Child = new LoginStatus();

WebContext.Current.Authentication.LoggedIn += new System.EventHandler<System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs>(Authentication_LoggedIn);

WebContext.Current.Authentication.LoggedOut += new System.EventHandler<System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs>(Authentication_LoggedOut);

}

void Authentication_LoggedOut(object sender, System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs e)

{

// скрыть станицы, недоступные для незарегистрированных пользователей

this.ContentFrame.Navigate(new Uri("/Home", UriKind.Relative));

}

void Authentication_LoggedIn(object sender, System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs e)

{

if (WebContext.Current.User.IsAdmin)

{

// показывать больше страниц

}

}

/// <summary>

/// After the Frame navigates, ensure the <see cref="HyperlinkButton"/> representing the current page is selected

/// </summary>

private void ContentFrame_Navigated(object sender, NavigationEventArgs e)

{

foreach (UIElement child in LinksStackPanel.Children)

{

HyperlinkButton hb = child as HyperlinkButton;

if (hb != null && hb.NavigateUri != null)

{

if (hb.NavigateUri.ToString().Equals(e.Uri.ToString()))

{

VisualStateManager.GoToState(hb, "ActiveLink", true);

}

else

{

VisualStateManager.GoToState(hb, "InactiveLink", true);

}

}

}

}

/// <summary>

/// If an error occurs during navigation, show an error window

/// </summary>

private void ContentFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)

{

e.Handled = true;

ErrorWindow.CreateNew(e.Exception);

}

}

}

Дальше мы сделаем страницу создания нового пользователя, а также добавления пользователей к проектам.

Немного об аутентификации в Silverlight 4 - Часть 1

Крайне важной задачей в любом бизнес-приложении является построение надежной системы аутентификации и авторизации.

Сейчас мы рассмотрим, как мне кажется, достаточно жизненный сценарий, в котором информация обо всех пользователях хранится в базе данных.

Также в базе будет храниться информация о связях каждого пользователя с определенным проектом. Причем пользовать может иметь одну из заранее определенных ролей в каждом из проектов.

Пользователи также могут иметь глобальную роль Администратора.

Запросы для создания нужной структуры базы данных могут выглядеть так:

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[Users](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Login] [nvarchar](50) NOT NULL,

[Password] [nvarchar](50) NOT NULL,

[Name] [nvarchar](50) NULL,

[IsAdmin] [bit] NOT NULL,

CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],

CONSTRAINT [IX_Users] UNIQUE NONCLUSTERED

(

[Login] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Users] ADD CONSTRAINT [DF_Users_IsAdmin] DEFAULT ((0)) FOR [IsAdmin]

GO

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[Projects](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Name] [nvarchar](100) NOT NULL,

[BankName] [nvarchar](100) NULL,

[IsDeleted] [bit] NOT NULL,

[GUID] [uniqueidentifier] NOT NULL,

CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Projects] ADD CONSTRAINT [DF_Projects_IsDeleted] DEFAULT ((0)) FOR [IsDeleted]

GO

ALTER TABLE [dbo].[Projects] ADD CONSTRAINT [DF_Projects_GUID] DEFAULT (newid()) FOR [GUID]

GO

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[ProjectRoles](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Name] [nvarchar](50) NOT NULL,

CONSTRAINT [PK_ProjectRoles] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

INSERT INTO [MyApplication].[dbo].[ProjectRoles] ([Name]) VALUES ('Manager')

GO

INSERT INTO [MyApplication].[dbo].[ProjectRoles] ([Name]) VALUES ('Editor')

GO

INSERT INTO [MyApplication].[dbo].[ProjectRoles] ([Name]) VALUES ('Viewer')

GO

------------------------------------------------------------

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[UserProjects](

[ID] [int] IDENTITY(1,1) NOT NULL,

[UserID] [int] NOT NULL,

[ProjectID] [int] NOT NULL,

CONSTRAINT [PK_UserProjects] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

ALTER TABLE [dbo].[UserProjects] WITH CHECK ADD CONSTRAINT [FK_UserProjects_Projects] FOREIGN KEY([ProjectID])

REFERENCES [dbo].[Projects] ([ID])

GO

ALTER TABLE [dbo].[UserProjects] CHECK CONSTRAINT [FK_UserProjects_Projects]

GO

ALTER TABLE [dbo].[UserProjects] WITH CHECK ADD CONSTRAINT [FK_UserProjects_Users] FOREIGN KEY([UserID])

REFERENCES [dbo].[Users] ([ID])

GO

ALTER TABLE [dbo].[UserProjects] CHECK CONSTRAINT [FK_UserProjects_Users]

GO

Теперь можно перейти к системе аутентификации. Можно заметить, что проект Silverlight Business Application для Silverlight 4 сразу создает несколько файлов, которые связаны с системой аутентификации.

В папке Model хранится файл User.cs, в папке Model/Shared хранится файл user.shared.cs. А в папке Services находится AuthenticationService.cs.

Изначально файлы содержат мало интересного, но стоит их рассмотреть:

User.cs содержит описание класса User, который будет использоваться системой аутентификации, поля класса из этого файла могут быть использованы из Silverlight проекта.

Позже мы добавим сюда несколько полезных свойств.

namespace MyApplication.Web

{

using System.Runtime.Serialization;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Collections.Generic;

/// <summary>

/// Class containing information about the authenticated user.

/// </summary>

public partial class User : UserBase

{

//// NOTE: Profile properties can be added for use in Silverlight application.

//// To enable profiles, edit the appropriate section of web.config file.

////

//// public string MyProfileProperty { get; set; }

/// <summary>

/// Gets and sets the friendly name of the user.

/// </summary>

public string FriendlyName { get; set; }

}

}

User.shared.cs пока содержит мало кода, в этом файле должны помещаться свойства, которые будут доступны как серверному так и клиентскому приложению. У меня это файл так и остался пустым, но, пожалуй, какие-то свойства в него добавиться можно.

using MyApplication.BLL;

namespace MyApplication.Web

{

/// <summary>

/// Partial class extending the User type that adds shared properties and methods

/// that will be available both to the server app and the client app

/// </summary>

public partial class User

{

/// <summary>

/// Returns the user display name, which by default is its Friendly Name,

/// and if that is not set, its User Name

/// </summary>

public string DisplayName

{

get

{

if (!string.IsNullOrEmpty(this.FriendlyName))

{

return this.FriendlyName;

}

else

{

return this.Name;

}

}

}

}

}

AuthenticationService.cs содержит, пожалуй, самый важный класс – AuthenticationService. Это и есть сервер для аутентификации. Пока что в нем нет никаких методов, но, поскольку он наследуется от уже готового класса, какая-то работа в нем все равно производится. Это очевидно посколько уже сейчас можно зарегистрироваться и залогиниться в приложение.

namespace MyApplication.Web

{

using System.Security.Authentication;

using System.ServiceModel.DomainServices.Hosting;

using System.ServiceModel.DomainServices.Server;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Threading;

using System.Security.Principal;

using HLUser = MyApplication.BLL.User;

using MyApplication.BLL;

using MyApplication.Web.Services;

/// <summary>

/// RIA Services DomainService responsible for authenticating users when

/// they try to log on to the application.

///

/// Most of the functionality is already provided by the base class

/// AuthenticationBase

/// </summary>

[EnableClientAccess]

public class AuthenticationService : AuthenticationBase<User>

{

}

}

Позже мы будем переопределять методы AuthenticationService для того, чтобы он поддерживал нашу аутентификацию, использующую таблицы из базы данных.

Пока же сделаем еще некоторые приготовления, нужно еще добавить в доменный сервис несколько методов для работы с пользователями, проектами и ролями.

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 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);

}

// TODO:

// Consider constraining the results of your query method. If you need additional input you can

// add parameters to this method or create additional query methods with different names.

// To support paging you will need to add ordering to the 'UserProjects' query.

public IQueryable<UserProject> GetUserProjects()

{

return this.ObjectContext.UserProjects;

}

public void InsertUserProject(UserProject userProject)

{

if ((userProject.EntityState != EntityState.Detached))

{

this.ObjectContext.ObjectStateManager.ChangeObjectState(userProject, EntityState.Added);

}

else

{

this.ObjectContext.UserProjects.AddObject(userProject);

}

}

public void UpdateUserProject(UserProject currentUserProject)

{

this.ObjectContext.UserProjects.AttachAsModified(currentUserProject, this.ChangeSet.GetOriginal(currentUserProject));

}

public void DeleteUserProject(UserProject userProject)

{

if ((userProject.EntityState == EntityState.Detached))

{

this.ObjectContext.UserProjects.Attach(userProject);

}

this.ObjectContext.UserProjects.DeleteObject(userProject);

}

// TODO:

// Consider constraining the results of your query method. If you need additional input you can

// add parameters to this method or create additional query methods with different names.

// To support paging you will need to add ordering to the 'Projects' query.

public IQueryable<Project> GetProjects()

{

var user = this.ObjectContext.Users.FirstOrDefault(p => p.Login == this.ServiceContext.User.Identity.Name);

if (user == null)

{

Logger.Log.ErrorFormat("No user {0} found", this.ServiceContext.User.Identity.Name);

return null;

}

if (user.IsAdmin)

{

return this.ObjectContext.Projects.Where(p => !p.IsDeleted);

}

return from up in this.ObjectContext.UserProjects.Include("Project")

where up.UserID == user.ID && !up.Project.IsDeleted

select up.Project;

}

public IQueryable<Project> GetProjectsManagedByUser()

{

var user = this.ObjectContext.Users.FirstOrDefault(p => p.Login == this.ServiceContext.User.Identity.Name);

if (user == null)

{

Logger.Log.ErrorFormat("No user {0} found", this.ServiceContext.User.Identity.Name);

return null;

}

if (user.IsAdmin)

{

return this.ObjectContext.Projects.Where(p => !p.IsDeleted);

}

return from up in this.ObjectContext.UserProjects.Include("Project").Include("ProjectRole")

where up.UserID == user.ID && !up.Project.IsDeleted && up.ProjectRole.Name == "Manager"

select up.Project;

}

public void InsertProject(Project project)

{

if ((project.EntityState != EntityState.Detached))

{

this.ObjectContext.ObjectStateManager.ChangeObjectState(project, EntityState.Added);

}

else

{

this.ObjectContext.Projects.AddObject(project);

}

}

public void UpdateProject(Project currentProject)

{

this.ObjectContext.Projects.AttachAsModified(currentProject, this.ChangeSet.GetOriginal(currentProject));

}

public void DeleteProject(Project project)

{

if ((project.EntityState == EntityState.Detached))

{

this.ObjectContext.Projects.Attach(project);

}

project.IsDeleted = true;

this.ObjectContext.SaveChanges();

}

Мы добавили несколько интересных запросов помимо стандартных методов Get, Update, Insert, Delete. Также стоит обратить внимание на то, что уже сейчас мы возвращаем не все проекты, а только те, которые доступны пользователю.