Разработка приложений на базе WPF и Silverlight

Разработка многофункциональных бизнес-приложений

Разбить на страницы
Показывать лекцию целиком

Цель

Освоить основы технологии разработки бизнес-приложение Silverlight на основе WCF RIA Services.

Общие сведения о насыщенных Silverlight-приложениях

При изучении лекций 6 и 7 построение Silverlight-приложения осуществлялось с реализацией слоя бизнес-логики, в основном, на стороне клиента. В корпоративных приложениях возникает необходимость переноса основной бизнес-логики или её части на сервер. Это диктуется требованиями обеспечения эффективной эксплуатации и сопровождения информационных систем.

Для решения таких задач Microsoft предлагает службы Windows Communication Foundation (WCF) RIA Services. RIA (Rich Internet Application) — это приложение, доступное через интернет, с богатой функциональностью традиционных настольных приложений [].

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

В Visual Studio 2010 имеются шаблоны для создания Silverlight бизнес-приложений с поддержкой служб RIA Services, которые включают реализацию многих функций, характерных для корпоративных приложений, таких как элементы управления для аутентификации, авторизации и регистрации пользователей, механизмы навигации, с помощью которых можно расширять функциональность приложения, добавляя новые страницы Silverlight.

Исследуем возможности построения корпоративных приложений на основе шаблона бизнес-приложений и служб WCF RIA Services при конструировании Silverlight-приложения.

Создание проекта Silverlight бизнес-приложения.

Создадим новый проект, используя шаблон Silverlight Business Application, задав для него имя ).

(рис 10.1) Страница создания проекта

Инструментальная среда Visual Studio 2010 в соответствии с выбранным шаблоном генерирует новое решение, состоящее из двух проектов (рис 10.2).

(рис 10.2) Главная страница проекта EnterpriceBusinessApplication

В сгенерированном решении имеются два проекта: клиентский Silverlight-проект EnterpriceBusinessApplication и серверный Web-проект EnterpriceBusinessApplication.Web. Каждый проект структурирован, включает папки и отдельные файлы, назначение которых будет рассмотрено позднее (рис 10.3)

(рис 10.3) Состав решения Enterprice BusinessApplication

При запуске проекта на экран выводится главная страница приложения (рис 10.4).

(рис 10.4) Главная страница приложения

На главной странице имеются три гиперссылки: Home – переход на главную страницу, About – переход на страницу с данными о программе, которые должен подготовить разработчик, login – для перехода на окно авторизации пользователя.

Для аутентификации пользователя предлагается форма авторизации (рис 10.5), а для регистрации нового пользователя – окно регистрации (рис 10.6).

Как видно из рис. 8.4 – 8.6 все надписи на страницах приложения приведены на английском языке, что является неудобными для пользователей России. Этот недостаток созданного приложения можно исправить путем замены в коде соответствующих английских строк на русские. Однако в шаблоне бизнес-приложения Silverlight предусмотрен более изящный способ локализации приложения на разные языки с помощью ресурсов. Так, например свойство Text для текстового блока HeaderText задается в расширении разметки ссылкой на статический ресурс.

<TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}" 
	Text="{Binding Path=ApplicationStrings.HomePageTitle, 
	Source={StaticResource ResourceWrapper}}"/>
(рис 10.6) Авторизация пользователя(рис 10.5) Регистрация пользователя

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

Для локализации приложения вначале необходимо отсоединить клиентскую часть – проект ).

(рис 10.7) Выгрузка клиентской части проекта

Для редактирования XAML-описания отсоединенного проекта в контекстном меню выберем пункт ).

(рис 10.8) Редактирование проекта

В XAML-описании проекта для элемента SupportedCultures добавим код для культуры русского языка ru-RU.

<SupportedCultures>en-US,ru-RU</SupportedCultures>

После сохранения изменений требуется перезагрузить проект (рис 10.9).

(рис 10.9) Перезагрузка проекта

Для клиентского проекта а). Копию ресурса следует переименовать в б).

(рис 10.10) Файл ресурса строк приложения

Двойным щелчком на файле ресурса .

(рис 10.11) Ресурс строк приложения с английскими значениями

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

(рис 10.12) Ресурс строк приложения с русскими значениями строк

Аналогичную последовательность действий по копированию, переименованию и переводу значений строк необходимо выполнить для файлов ресурсов клиентского приложения ErrorResources.resx и SecurityQuestions.resx, а также файлов RegistrationDataResources.resx и ValidationErrorResources.resx серверного проекта EnterpriceBusinessApplication.Web.csproj.

Для серверной части проекта необходимо внести изменения в файл EnterpriceBusinessApplicationTestPage.aspx, добавив в головной элемент XAML-описания атрибуты UICulture и Culture.

<%@ Page Language="C#" AutoEventWireup="true" UICulture="auto" Culture="auto"%>

Измененный файл EnterpriceBusinessApplicationTestPage.aspx необходимо сохранить.

Для настройки приложения на русский язык достаточно в свойствах Internet Explorer установить первым в списке языков русский язык (рис 10.13).

(рис 10.13) Установка языка для Internet Explorer

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

(рис 10.15) Главная страница приложения(рис 10.14) Авторизация пользователя(рис 10.16) Регистрация пользователя

Создание модели и сервисов данных

Создание EDM-модели данных рассматривалось в лекциях 6 и 7. Исходной базой данных для EDM-модели модели является существующая база данных сотрудников организации – PersonalEnterprice. В Web-проект добавим папку ).

(рис 10.17) Модель данных

Таблица .

Свойства таблицы Employee
Свойство Назначение
ID Суррогатный ключ
FirstName Имя
SecondName Отчество
LastName Фамилия
TitleID Внешний ключ для таблицы Title
Role Роль/группа
Email Адрес электронной почты
Phone Телефон
Inn ИНН
FirstData Дата приема на работу
LastDate Дата увольнения
Picture Фотография
Title Связь с таблицей Title
Свойства таблицы Title
Свойство Назначение
ID Суррогатный ключ
Title1 Должность
Employees Связь с таблицей Employee

При генерации EDM-модели создается класс PersonalEnterpriceEntities, который наследуется от класса ObjectContext. Класс PersonalEnterpriceEntities содержит описание сущностей базы данных PersonalEnterprice как свойств, метод добавления сущностей и навигации. После создания EDM-модели необходимо перекомпилировать проект.

Следующим шагом проектирования приложения является создание службы WCF RIA Services. В Web проекте создадим папку ).

(рис 10.18) Добавление Domain Service Class

При добавлении сервиса WCF RIA Services необходимо в окне ).

(рис 10.19) Настройка добавления Domain Service Class

В результате добавления EDM-модели и сервиса WCF RIA Services в проекте будут сформированы в папке ).

(рис 10.20) Обозреватель решений проекта. Серверная часть

Созданный серверный класс EmployeeDomainService является, в общем случае, потомком класса DomainService, в нашем случае – это потомок класса LinqToEntitiesDomainService<PersonalEnterpriceEntities>, который обеспечивает доступ к данным на стороне сервера. Код класса EmployeeDomainService приведен в приложении.

Класс EmployeeDomainService помечен атрибутом [ EnableClientAccess() ], который обеспечивает доступ к сервису со стороны клиента. Методы GetEmployees() и GetTitles() класса предназначены для загрузки в приложение данных из базы, а методы Insert(), Update() и Delete() – для манипулирования данными.

Класс EmployeeDomainService.metadata.cs содержим метаданные о сущностях модели данных. Код класса EmployeeMetadata приведен в приложении.

Для разрабатываемого приложения необходима связь от таблицы Employee к таблице Title, поэтому в класс EmployeeMetadata необходимо добавить коллекцию Titles.

EntityCollection<Title> Titles { get; set; }

Метаданные используются, в частности, для генерации элементов контроля в клиентской части приложения и валидации данных. Свойства данных задаются с помощью атрибутов пространства имен System.ComponentModel.DataAnnotations. Если свойства не помечать специальными атрибутами, то в элементах контроля им будут присваиваться метки в соответствии с именами свойств, например FirstName, а порядок формирования элементов, например в элементе контроля DataGrid, будет соответствовать порядку данных в таблице и соответственно для сущности данных. Так, если для свойства Picture необходимо задать метку Фото и в списке свойств в таблице поместить её на первое место, то необходимо для атрибута Display определить свойства Name и Order.

[Display(Name = "Фото", Order = 0)]
public byte[ ] Picture { get; set; }

Перечень свойств атрибута Display приведен в табл.8.3

Свойства атрибута Display
Имя Описание
AutoGenerateField Получает или задает значение, указывающее, нужно ли для отображения этого поля автоматически создавать пользовательский интерфейс.
AutoGenerateFilter Получает или задает значение, которое используется для фильтрации.
Description Получает или задает значение, которое используется для отображения описания пользовательского интерфейса.
GroupName Получает или задает значение, используемое для группировки полей в пользовательском интерфейсе.
Name Получает или задает значение, которое используется для отображения в элементе пользовательского интерфейса.
Order Получает или задает порядковый вес столбца.
Prompt Получает или задает значение, которое будет использоваться для задания подсказки в элементе пользовательского интерфейса.
ResourceType Получает или задает тип, содержащий ресурсы для свойств ShortName, Name, Prompt и Description.
ShortName Получает или задает значение, используемое в качестве метки столбца сетки.
TypeId При реализации в производном классе возвращает уникальный идентификатор для этого Attribute.

Для свойств класса EmployeeMetadata необходимо указать атрибут [ Display ], который будет использованы в интерфейсных элементах. Для атрибута Titles дополнительно укажем атрибут [ Include ], который предписывает сформировать коллекцию объектов Title в соответствии со свойствами навигации. Код модифицированного класса EmployeeMetadata приведен в приложении.

При построении сервиса WCF RIA Services на клиенте автоматически генерируется файл ).

(рис 10.21) Обозреватель решений проекта. Клиентская часть

В файле EnterpriceBusinessApplication.Web.g.cs, в частности, имеются классы сущностей, аналогичные классам серверной части приложения и класс EmployeeDomainContext, который является наследником класса DomainContext и обеспечивает доступ к данным и методам сервиса со стороны клиента.

Разработка клиентской части приложения

Разработаем страницу для отображения списка сотрудников. Добавим в клиентскую часть, в папку ).

(рис 10.22) Вкладка Источник данных

Перетащим источник данных для таблицы .

(рис 10.23) Конструктор страницы EmployeePage

Добавим на главной странице Home гиперссылку на созданную страницу EmployeePage, указав для контента расширение разметки для ссылки на строку ресурса ApplicationStrings.EmployeePageTitle.

<HyperlinkButton x:Name="Link3" Style="{StaticResource LinkStyle}"
	NavigateUri="/EmployeePage" TargetName="ContentFrame" 
	Content="{Binding Path=ApplicationStrings.EmployeePageTitle, 
	Source={StaticResource ResourceWrapper}}"/>

После компиляции и запуска приложения страница .

(рис 10.24) Страница EmployeePage – первоначальный вариант

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

Добавим в папку Helpers клиентского приложения класс конвертора ImageConverter. Аналогичный конвертор использовался в лекции 7.

Добавим в ресурсы приложения ресурс конвертора с ключом ImageConverter.

<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Assets/Styles.xaml"/> 
                <ResourceDictionary>
                    <app:ResourceWrapper x:Key="ResourceWrapper" />
                    <app:NotOperatorValueConverter x:Key="NotOperatorValueConverter" />
                    <app:ImageConverter x:Key="ImageConverter" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>                
    </Application.Resources>

Изменим XAML-описание для столбца Фото сетки employeeDataGrid.

<sdk:DataGridTemplateColumn x:Name="pictureColumn" Header="Фото" Width="SizeToHeader">
  <sdk:DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
       <Image Source="{Binding Path=Picture, 
                  Converter={StaticResource ImageConverter}}" 
                  Margin="1" Stretch="UniformToFill" Height="60" />
    </DataTemplate>
  </sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>

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

Для отображения должности сотрудника в сетке employeeDataGrid на странице EmployeePage необходимо при загрузке сущности Employee обеспечить загрузку связанных с ней данных из таблицы Title. Это можно сделать модифицировав метод GetEmployees() класса сервиса данных EmployeeDomainService.

public IQueryable<Employee> GetEmployees()
{
    return this.ObjectContext.Employees.Include("Title").OrderBy(e => e.LastName);
}

В методе GetEmployees() используется класс ObjectContext, который предоставляет возможность выполнять запросы и работать с данными сущности как с объектами. Данный класс обеспечивает взаимодействие с данными как с объектами, которые являются экземплярами типов сущности, определенными в модели EDM.

Метод GetEmployees() возвращает экземпляр класса IQueryable, который представляет типизированный запрос по отношению к концептуальной модели в данном контексте объекта. Класс имеет ряд методов для формирования запросов к модели данных, некоторые из которых приведены в табл. 8.4.

Методы класса ObjectContext
Имя Описание
GroupBy Группирует результаты запроса по заданным критериям.
Include Задает связанные объекты, включаемые в результаты запроса.
Intersect Ограничивает результаты запроса, включая только те, которые вошли в результаты другого запроса объектов.
OrderBy Сортирует результаты запроса по заданным критериям.
Select Ограничивает результаты запроса только теми свойствами, которые определены в заданной проекции.
Skip Сортирует результаты запроса по заданным критериям и пропускает указанное число результатов.
Top Ограничивает результаты запроса указанным числом элементов.
Where Ограничивает запрос результатами, соответствующими определенным критериям фильтрации.

В методе GetEmployees() для сущности Employees используется метод Include() для объединение в запросе сущностей Employees и Titles, а также метод OrderBy() для сортировки результатов запроса по фамилии ( e => e.LastName ).

В XAML-описании колонки Должность сетки employeeDataGrid страницы EmployeePage следует изменить путь привязки, чтобы получить данные о должности ( Title1 ) из сущности Title.

<sdk:DataGridTextColumn x:Name="titleColumn" 
	Binding="{Binding Path=Title.Title1}" 
	Header="Должность" Width="150" />

После компиляции и запуска программы страница .

(рис 10.25) Страница Сотрудники

При загрузке страницы Сотрудники приложения происходит загрузка данных из базы данных, что требует определенного времени. Во время загрузки данных на экране дисплея появляется пустая страница и не понятно работает или нет приложение. Устранение такой неопределенности можно реализовать с помощью класса BusyIndicator, построенного в соответствии с шаблоном проекта. Использование данного класса обеспечивает вывод элемента управления ProgressBar на экран во время выполнения длительной операции.

Объект BusyIndicator – индикатор длительно выполняемой операции является контейнером, в который необходимо поместить объект, куда должны быть выведены данные в результате выполнения операции. Для разрабатываемого приложения в контейнер BusyIndicatorLoadData поместим сетку employeeDataGrid, которая визуализирует данные о сотрудниках.

<local:BusyIndicator x:Name="BusyIndicatorLoadData"
	IsBusy="{Binding IsLoadingData, ElementName= employeeDomainDataSource}"
	BusyContent="{Binding Path=ApplicationStrings.BusyIndicatorLoadData, 
	Source={StaticResource ResourceWrapper}}" >
	<sdk:DataGrid  Name="employeeDataGrid" . . .>
	....
	</sdk:DataGrid.Columns>
</local:BusyIndicator>

Свойство IsBusy объекта BusyIndicator задает состояние отображения элемента управления ProgressBar. Данное свойство с помощью расширения разметки привязывается к свойству IsLoadingData объекта DomainDataSource – employeeDomainDataSource. Свойство BusyContent определяет строку, которая будет выводиться в элементе управления ProgressBar при его отображении. В расширении разметки для свойства BusyContent используется задание пути ( ApplicationStrings.BusyIndicatorLoadData ) к статическому ресурсу. При загрузке данных на экране будет отображаться элемент управления ).

(рис 10.26) Индикация загрузки данных

При разработке дизайна страницы приложения имеются много вариантов визуализации данных. Например, можно реализовать организацию разбиения и перелистывания страниц с использованием объекта DataPager. Добавим в XAML-описание страницы EmployeePage объект DataPager после описания индикатора загрузки данных BusyIndicatorLoadData.

<sdk:DataPager Height="26" HorizontalAlignment="Left" 
	VerticalAlignment="Bottom" Margin="25,0,0,35" 
	Name="dataPager1"  Width="200" 
	Source="{Binding ElementName=employeeDomainDataSource, Path=Data}" 
	PageSize="5" />

Источником данных ( ).

(рис 10.27) Реализация перелистывания страниц элементом PageSize

Еще одним возможным вариантом визуализации данных в DataGrid является использование группировки по определенному полю. Предположим, что требуется провести группировку по должности сотрудников. Для этого необходимо в XAML-описание объекта DomainDataSource добавить свойство GroupDescriptors, указав для атрибута PropertyPath, который определяет свойство группировки, значение поля, по которому будет проводиться группировка – Title.Title1.

<riaControls:DomainDataSource AutoLoad="True" 
	d:DesignData="{d:DesignInstance my:Employee, CreateList=true}" 
	Height="0" Width="0"
	LoadedData="employeeDomainDataSource_LoadedData"
	Name="employeeDomainDataSource"
	QueryName="GetEmployeesQuery" >
	<riaControls:DomainDataSource.DomainContext>
		<my1:EmployeeDomainContext />
	</riaControls:DomainDataSource.DomainContext>
	<riaControls:DomainDataSource.GroupDescriptors>
		<riaControls:GroupDescriptor PropertyPath="Title.Title1"/>
	</riaControls:DomainDataSource.GroupDescriptors>
</riaControls:DomainDataSource>

На рис 10.28 приведены результаты работы приложения с группировкой сотрудников по должности. В списке сотрудников для каждой должности указывается количество записей в группе, группу можно свернуть и развернуть. Так на рис 10.28 развернутой является только группа, соответствующая должности "Начальник клиентского отдела".

(рис 10.28) Реализация группировки на странице Сотрудники

Следующим этапом разработки приложения является организация поиска/фильтрации данных по фамилии сотрудников и по должности.

Вначале модифицируем макет страницы EmployeePage. Добавим в сетку LayoutRoot две строки и три столбца.

Контейнер BusyIndicatorLoadData поместим во второй строке, сетки LayoutRoot, объединив для этого три столбца.

<local:BusyIndicator x:Name="BusyIndicatorLoadData" 
	Grid.Row="1" Grid.ColumnSpan="3" ...>

В первой строке и колонки LayoutRoot разметим объект TextBlock с текстом "Список сотрудников".

Во второй колонке первой строки LayoutRoot поместим элементы управления необходимые для задания параметров фильтрации данных: текстовые блоки с текстами "Поиск", "По фамилии" и "По должности", блок для ввода фамилии textBoxSurname и элемент контроля AutoCompleteBox для ввода должности. Все перечисленные элементы контроля скомпонованы в сетке gridSearch, которая помещена в рамку borderSearch. XAML-описание фрагмента страницы Сотрудники сетки gridSearch, визуализирующей элементы контроля для поиска данных по сотрудникам, приведены в приложении.

Для фильтрации данных по фамилии сотрудника в XAML-описание объекта DomainDataSource необходимо добавить объект FilterDescriptors, который представляет описание фильтра для операций запроса в DomainDataSource объекте.

<riaControls:DomainDataSource.FilterDescriptors>
	<riaControls:FilterDescriptor Operator="StartsWith" 
		PropertyPath="LastName" 
		Value="{Binding ElementName= textBoxLastName, Path=Text}" />
</riaControls:DomainDataSource.FilterDescriptors>

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

Значения перечисления FilterDescriptors
Значение оператора фильтрации Описание действий
Contains Значение должно содержать значение фильтра.
EndsWith Значение должно заканчиваться значением фильтра.
IsContainedIn Значение данных должны содержаться в значении фильтра.
IsEqualTo Значение должно быть равно значению фильтра.
IsGreaterThan Значение должно быть больше, чем значение фильтра.
IsGreaterThanOrEqualTo Значение должно быть больше или равно значению фильтра.
IsLessThan Значение должно быть меньше значения фильтра.
IsLessThanOrEqualTo Значение должно быть меньше или равно значению фильтра.
IsNotEqualTo Значение должно отличаться от значения фильтра.
StartsWith Значение должно начинаться с символов, заданных в фильтре.

В созданном фильтре для фамилии сотрудника использован оперетор .

(рис 10.29) Реализация фильтрации по фамилии

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

В папку DomainModel добавим класс TitleEmployee для формирования данных о должности сотрудников, имеющихся в сущности Employee.

public class TitleEmployee
{
    [Key]
    public string Title { set; get; }
}

Свойство Title снабжено атрибутом [ Key ] для обеспечения возможности формирования полей новой сущность на базе этого класса.

В класс сервиса данных EmployeeDomainService добавим метод GetTitlesEmployee() для получения списка должностей сотрудников.

public IQueryable<TitleEmployee> GetTitlesEmployee()
{
    return this.ObjectContext.Employees.Select(e => new TitleEmployee 
	{ Title = e.Title.Title1 }).Distinct();
}

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

<riaControls:DomainDataSource AutoLoad="True" 
	d:DesignData="{d:DesignInstance my1:TitleEmployee, CreateList=true}" Height="0"
	LoadedData="TitleEmployeeDomainDataSource_LoadedData"  Name="titleEmployeeDomainDataSource"
	QueryName="GetTitlesEmployeeQuery" Width="0">
	<riaControls:DomainDataSource.DomainContext>
		<my1:EmployeeDomainContext />
	</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>

Источник данных titleEmployeeDomainDataSource формирует список должностей с помощью метода GetTitlesEmployeeQuery() домена сервиса данных.

Для ввода должности при фильтрации формы используется элемент управления AutoCompleteBox, источником привязки которого служит объект данных titleEmployeeDomainDataSource, значение для данного элемента определяются полем Title.

<input:AutoCompleteBox Grid.Row="2" Grid.Column="1" Name="FilterText"
	HorizontalAlignment="Left"
	ValueMemberBinding="{Binding Title}" 
	ItemsSource="{Binding ElementName=titleEmployeeDomainDataSource, 
				Path=Data}"
	TextChanged ="FilterText_TextChanged"
	Margin="20,2,20,2" Height="23" Width="300"/>

Элемент управления AutoCompleteBox поддерживает формирование подсказки при вводе первых символов слова, если оно имеется в источнике данных.

В заключении процесса проектирования функции фильтрации данных по должности необходимо в источнике данных employeeDomainDataSource добавить в объект FilterDescriptors XAML-описание фильтра.

<riaControls:FilterDescriptor PropertyPath="Title.Title1" 
	Operator="IsEqualTo" Value="{Binding ElementName=FilterText, Path=Text}" />

Если в источнике данных указаны несколько объектов FilterDescriptor, то они могут работать по схеме "Или" ( Or ) и по схеме "И" ( And ). Режим объединения фильтров задается свойством FilterOperator источника данных и в проектируемом приложении ему задано значение Or.

На рис 10.30 иллюстрируется функциональность элемента управления – результаты работы приложения при выполнении фильтрации по должности.

(рис 10.31) Подсказка при вводе должности(рис 10.30) Результаты фильтрации по должности

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

На странице EmployeePage создадим кнопки buttonNewEmployee, buttonEdit и buttonDelete для реализации выполнения соответствующих операций.

Добавим в клиентскую часть приложения дочернее окно, с помощью которого будем редактировать и добавлять данные по сотрудникам. Для этого в помощнике ) и задать имя ).

(рис 10.32) Создание дочернего окна

В конструкторе окна ). Для атрибутов сущности

(рис 10.33) Источники данных

С помощью мыши перетащим сущность ).

(рис 10.34) Дочернее окно EmployeeChildWindow

В XAML-описание окна будет добавлен контекст данных для сущности Employee.

<riaControls:DomainDataSource AutoLoad="True" 
	d:DesignData="{d:DesignInstance my1:Employee, CreateList=true}" 
	Name="employeeDomainDataSource" Height="0" Width="0"
	LoadedData="employeeDomainDataSource_LoadedData" 
	QueryName="GetEmployeesQuery">
	<riaControls:DomainDataSource.DomainContext>
		<my:EmployeeDomainContext />
	</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>

Если проанализировать XAML-описание окна EmployeeChildWindow, то можно увидеть, что элемент Image для отображение фотографии требует корректировки, а элемент ComboBox не настроен правильно на источник данных. Для отображения фотографии в привязке необходимо использовать конвертор. В дополнение к этому поместим объект Image в рамку Border.

<Border Grid.Column="2" Name="Border"
	Padding="5" Width="200" Height="250" CornerRadius="10" 
	Background="#FFC8E5E9" Margin="3" VerticalAlignment="Center"
	ToolTipService.ToolTip=
	"Для изменения фотографии щелкните на ней правой кнопкой мыши" 
	MouseRightButtonDown="Border_MouseRightButtonDown" >
	<Image  Height="240" HorizontalAlignment="Center" Margin="3" 
		Name="pictureImage" 
		Source="{Binding Path=Picture, Converter={StaticResource ImageConverter}}" 
			Stretch="UniformToFill" VerticalAlignment="Center" Width="196" />
</Border>

Для подготовки источника данных для comboBoxTitle необходимо в XAML-описание окна EmployeeChildWindow добавить контекст данных для сущности Title.

<riaControls:DomainDataSource AutoLoad="True" 
	d:DesignData="{d:DesignInstance my1:Title, CreateList=true}" 
	LoadedData="titleDomainDataSource_LoadedData" 
	Name="titleDomainDataSource" 
	QueryName="GetTitlesQuery">
	<riaControls:DomainDataSource.DomainContext>
		<my:EmployeeDomainContext />
	</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>

После этого можно отредактировать описание для выпадающего списка должностей.

<ComboBox x:Name="comboBoxTitle" Height="23" Grid.Column="2" Grid.Row="4" 
	HorizontalAlignment="Left" 
	ItemsSource="{Binding ElementName=titleDomainDataSource, Path=Data}"
	DisplayMemberPath="Title1" 
	SelectedValuePath="ID" 
	SelectedValue="{Binding Path=TitleID,  Mode=TwoWay,
				  UpdateSourceTrigger=Default}"  
	Margin="3,3,0,0" VerticalAlignment="Top" Width="198">
</ComboBox>

Теперь следует создать вызов дочернего окна из страницы EmployeePage при нажатии кнопки buttonEdit.

private void buttonEdit_Click(object sender, RoutedEventArgs e)
{
	Employee employeeEdit = employeeDataGrid.SelectedItem as Employee;
	if (employeeEdit != null)
	{
	EmployeeChildWindow winEmpEdit = new EmployeeChildWindow(employeeEdit);
	winEmpEdit.Closed += new EventHandler(winEmpEdit_Closed);
	winEmpEdit.Title = "Редактирование данных";
	winEmpEdit.Show();
	}
}

В обработчике нажатия кнопки buttonEdit создается экземпляр employeeEdit сущности Employee на основе выделенной в текущий момент строки сетки данных employeeDataGrid.

Employee employeeEdit = employeeDataGrid.SelectedItem as Employee;

Если объект employeeEdit существует, то создается экземпляр winEmpEdit дочернего окна EmployeeChildWindow, которому в качестве параметра передается экземпляр сущности данных employeeEdit.

EmployeeChildWindow winEmpEdit = new EmployeeChildWindow(employeeEdit);

Затем включают прослушивания события закрытия окна.

winEmpEdit.Closed += new EventHandler(winEmpEdit_Closed);

Для дочернего формируется заголовок и окно выводится на экран дисплея.

winEmpEdit.Title = "Редактирование данных";
winEmpEdit.Show();

Обработка события закрытия дочернего окра реализуется методом winEmpEdit_Closed(), в котором по результату нажатия кнопки ОК проводят сохранение изменений в базе данных

employeeDomainDataSource.SubmitChanges();

а при нажатии кнопки Отмена – отмена проведенного редактирования данных

employeeDomainDataSource.RejectChanges();

Полный код обработчика winEmpEdit_Closed приводится ниже.

void winEmpEdit_Closed(object sender, EventArgs e)
{
    EmployeeChildWindow emp = (EmployeeChildWindow)sender;
    if (emp.Employee != null)
    {
        if (emp.DialogResult == true)
        {
            employeeDomainDataSource.SubmitChanges();
        }
        else
        {
            employeeDomainDataSource.RejectChanges();
        }
    }
}

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

(рис 10.35) Редактирование данных

Текстовые поля редактируются непосредственно в поле, должность выбирается из выпадающего списка, даты задаются с помощью выпадающего календаря, а при наведении курсора на фотографию выводится подсказка "Для изменения фотографии щелкните на ней правой кнопкой мыши" и для корректировки фотографии по нажатию правой кнопки мыши выводится окно "Открыть" для задания нового файла фотографии. Для завершения редактирования нажимается кнопка ОК, а для отмены редактирования – кнопка Отмена.

Формирование данных по новому сотруднику будем выполнять с использованием созданного ранее дочернего окна EditEmployeeChildWindow. Для того, чтобы в коде можно было различать режимы в котором находится дочернее окно будем использовать свойство Tag окна. Так при формировании окна в режиме редактирования свойству Tag присвоим значение edit, а при добавлении нового сотрудника – new.

private void buttonNew_Click(object sender, RoutedEventArgs e)
        {
            Employee  newEmployee= new Employee();
            newEmployee.Role= "guest";
            newEmployee.LastName= "не задано";
            newEmployee.FirstName = "не задано";
            newEmployee.SecondName = "не задано";
            newEmployee.Email = "a@my.com";
            newEmployee.Phone = "(999) 999-9999";
            newEmployee.Inn = "999999999999";
            newEmployee.TitleID = 1;
            newEmployee.FirstDate = DateTime.Now;
            EmployeeChildWindow winEmpEdit = new 
			EmployeeChildWindow(newEmployee);
            winEmpEdit.Title = "Регистрация нового сотрудника";
            winEmpEdit.Tag = "new";
            winEmpEdit.Closed += new EventHandler(winEmpEdit_Closed);
            winEmpEdit.Show();
        }

В методе buttonNew_Click() создается экземпляр класса Employee, задаются его поля, затем по аналогии с режимом редактирования данных, создается экземпляр дочернего окна EmployeeChildWindow, задаются его свойства, включается прослушивание события закрытия окна и оно выводится на экран.

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

void winEmpEdit_Closed(object sender, EventArgs e)
        {
            EmployeeChildWindow emp = (EmployeeChildWindow)sender;
            switch (emp.Tag.ToString())
            {
                case "edit":
                    if (emp.Employee != null)
                    {
                        if (emp.DialogResult == true)
                        {
                            employeeDomainDataSource.SubmitChanges();
                        }
                        else
                        {
                            employeeDomainDataSource.RejectChanges();
                        }
                    }
                    break;
                case "new":
                    if (emp.Employee != null)
                    {
                        if (emp.DialogResult == true)
                        {
                            EmployeeDomainContext employeeDomain = 
(employeeDomainDataSource.DomainContext) as EmployeeDomainContext;
                            employeeDomain.Employees.Add(emp.Employee);
                            employeeDomainDataSource.SubmitChanges();
                        }
                     }
                    break;
            }
        }

При создании новой записи по сотруднику используется метод Add сущности контекста домена службы DomainService и изменения в источнике данных фиксируются методом SubmitChanges().

При работе приложения и нажатии кнопки ).

(рис 10.36) Добавление нового сотрудника

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

Удаление записей по сотруднику реализуются в приложении методом buttonDelete_Click(), который является обработчиком события Click кнопки buttonDelete.

private void buttonDelete_Click(object sender, RoutedEventArgs e)
        {
            Employee emp = (Employee)(employeeDataGrid.SelectedItem);
            if (emp != null)
            {
                MessageBoxResult result = MessageBox.Show("Удалить сотрудника: \n" +
                                            emp.LastName+"  " + emp.FirstName + "  "+  emp.SecondName,
                  "Предупреждение", MessageBoxButton.OKCancel);
                if (result == MessageBoxResult.OK)
                {
                    EmployeeDomainContext employeeDomain = 
                                        (employeeDomainDataSource.DomainContext) as 
						EmployeeDomainContext;
                    employeeDomain.Employees.Remove(emp);
                    employeeDomainDataSource.SubmitChanges();
                }
            }
        }

Удаление данных из источника реализуется методом ).

(рис 10.37) Удаление данных по сотруднику

Валидация ввода данных

Вопросы проверки правильности введенных данных, то есть валидации рассматривались в лекциях 3 и 4, в контексте привязки данных на уровне представления. Реализация такого варианта валидации осуществлялась на стороне клиента. Проверка правильности введенных данных относится к бизнес-логике приложения и его по возможности следует отделять от уровня представления данных. В корпоративных приложениях на базе сервисов WCF RIA Services имеется возможность реализации валидации на серверной стороне. Для этого используются атрибуты метаданных объектов из пространства имен System.ComponentModel.DataAnnotations. Мы уже рассматривали атрибут [ Display ] для задания представления имен свойств сущности Employee в файле метаданных EmployeeMetadata. Для валидации по умолчанию используются следующие атрибуты:

  • DataTypeAttribute – определяет тип для проверяемого свойства;
  • RangeAttribute – фиксирует диапазон возможных значений;
  • RegularExpressionAttribute – задает регулярное выражение для строковых данных;
  • RequiredAttribute – определяет обязательность свойства при вводе;
  • StringLengthAttribute – задает длину строки.
  • Валидацию свойства FirstName сущности Employee проведем для выполнения следующих условий:

  • вводимое имя может содержать только строчные и прописные буквы русского алфавита и символ пробела, для всех 20-ти возможных для ввода символов. При нарушении этого условия необходимо вывести сообщение "Недопустимые символы или более 20 символов";
  • свойство FirstName является обязательным для ввода и при нарушении этого условия вывести сообщение "Поле обязательно должно быть заполнено".
  • Фрагмент кода класса EmployeeMetadata в части описания свойства FirstName приведен ниже.

    [Display(Name = "Имя", Order = 2)]
    [RegularExpression((@"^[А-Яа-я' ']{1,20}$"),
                                 ErrorMessage = "Недопустимые символы или более 20 символов")]
    [Required(ErrorMessage = "Поле обязательно должно быть заполнено")]
    public string FirstName { get; set; }

    Аналогичные атрибуты установим в классе EmployeeMetadata для свойств LastName, SecondName и для свойства Role, за исключением атрибута [ Required ].

    Для свойства Email с помощью регулярного выражения будем проверять требуемый формат для электронной почты, а сообщение об ошибке должно формироваться ссылкой на ресурс.

    [Display(Name = "Эл. почта", Order = 7)]
    [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; }

    Для свойства Inn определим допустимость ввода только цифровых символов, которых может быть 12.

    [Display(Name = "ИНН", Order = 6)]
    [RegularExpression((@"^[0-9]{1,12}$"), 
    ErrorMessage = "Допустимы толька цифровые символы, длина ИНН - 12 символов")]
    [CustomValidation(typeof(ValidaterInn), "ValidateInn")]
    public string Inn { get; set; }

    Для свойства Inn применен атрибут CustomValidation, который позволяет задать пользовательское правило проверки. В параметрах атрибута CustomValidation определен класс ValidaterInn, определяющий правило проверки, и метод проверки – ValidateInn. Класс ValidaterInn создадим в папке Models/ Shared проекта EnterpriceBusinessApplication.Web.

    public class ValidaterInn
    {
        public static ValidationResult ValidateInn(string Inn)
        {
            ValidationResult res;
            if (Inn.Length == 12)
            {
                res = ValidationResult.Success;
            }
            else
            {
                res = new ValidationResult("ИНН должен содержать 12 символов");
            }
            return res;
        }
    }

    Статический метод .shared, то есть ValidaterInn.shared.cs.

    Код измененного класса EmployeeMetadata приведен в приложении.

    На рис 10.39, рис 10.40 приведены результаты тестирования процесса валидации приложения.

    (рис 10.40) Проверка ввода фамилии(рис 10.39) Проверка ввода адреса электронной почты и ИНН

    Аутентификация и авторизация пользователей

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

    Для настройки Windows- авторизации в проекте EnterpriceBusinessApplication.Web для файла Web.config необходимо свойству mode объекта authentication установить значение Windows.

    <authentication mode="Windows"/ >

    В клиентском проекте EnterpriceBusinessApplication в конструкторе класса App для свойства аутентификации Authentication веб конекста webContext присвоить значение вновь созданного экземпляра класса Windows аутентификации – WindowsAuthentication.

    public App()
    {
    	InitializeComponent();
    	WebContext webContext = new WebContext();
    	webContext.Authentication = new WindowsAuthentication();
    	this.ApplicationLifetimeObjects.Add(webContext);
    }

    После загрузки приложения происходит автоматически Windows-авторизация пользователя (рис 10.41).

    (рис 10.41) Windows-авторизация

    Для аутентификации зарегистрированных в приложении пользователей в проекте EnterpriceBusinessApplication.Web для файла Web.config необходимо свойству mode объекта authentication установить значение Forms и задать имя форы.

    <authentication mode="Forms">
          <forms name=".EnterpriceBusinessApplication_ASPXAUTH" />
    </authentication>

    В конструкторе клиентского класса App следует для свойства аутентификации Authentication веб контекста webContext создать экземпляр класса пользовательской авторизации FormsAuthentication.

    public App()
    {
    	InitializeComponent();
    	WebContext webContext = new WebContext();
    	webContext.Authentication = new FormsAuthentication();
    	this.ApplicationLifetimeObjects.Add(webContext);
    }

    Нового пользователя при открытии приложения необходимо зарегистрировать (рис 10.42). Зарегистрированный пользователь должен пройти аутентификацию (рис 10.43). Если аутентификация прошла успешно, то на странице приложения выводится имя пользователя, работающего с приложением (рис 10.44).

    (рис 10.43) Регистрация нового пользователя(рис 10.42) Аутентификация пользователя(рис 10.44) Вход в приложение зарегистрированного пользователя

    Если имя пользователя или пароль введены с ошибкой, то аутентификация пользователя не проходит (рис 10.45).

    (рис 10.45) Отказ в аутентификации пользователя

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

    Таким образом, аутентификация осуществляется на этапе регистрации пользователя, а авторизация реализуется на базе аутентификации с применением механизма ролей.

    Для создания ролей необходимо настроить конфигурацию ASP.NET приложения. Для проекта ).

    (рис 10.46) Выбор настройки конфигурации ASP.NET

    На странице администрирования веб-узла (рис 10.47) следует открыть вкладку ).

    (рис 10.48) Страница администрирования веб-узла(рис 10.47) Вкладка безопасность страницы администрирования веб-узла

    Выбрав ссылку "Создание ролей и управления ими" перейдем на страницу, где создадим две новые роли: администратор и оператор (рис 10.49).

    (рис 10.49) Создание ролей приложения

    Для создания новых пользователей выберем ссылку "Создать пользователя" и создадим два новых пользователя – одного с ролью Адмитистратор, а другого – "Оператор" (рис 10.50).

    (рис 10.50) Создание нового пользователя с заданием роли

    На странице администрирования веб-узла (рис 10.48) предоставляются возможности создания и управления пользователями, ролями и правилами доступа.

    Для разграничения прав доступа различных пользователей к приложению используются атрибуты для классов и методов службы домена. Если ставится задача ограничить доступ к таблице Employee только зарегистрированным пользователям, метод загрузки данных GetEmployees() для этой таблицы класса EmployeeDomainService должен быть отмечен атрибутом RequiresAuthentication().

    [RequiresAuthentication()]
    public IQueryable<Employee> GetEmployees()

    При переходе на страницу ).

    (рис 10.51) Отказ в доступе для незарегистрированного пользователя

    Если переход на страницу ).

    (рис 10.52) Разрешен доступ для зарегистрированного пользователя

    Если требуется разграничить роли пользователей при выполнении определенных операций, то следует применять атрибут RequiresRole ("Имя роли"). Так если оператору, как зарегистрированному пользователю, разрешено только просматривать список сотрудников, а операции модификации, удалении и создания новых данных по сотрудника разрешается выполнять пользователю с ролью администратор, соответствующие методы должны быть отмечены атрибутами RequiresRole (" администратор ").

    [RequiresRole("администратор")]
    public void InsertEmployee(Employee employee)
        { . . . }
    
    [RequiresRole("администратор")]
    public void UpdateEmployee(Employee currentEmployee)
            { . . . }
    
    [RequiresRole("администратор")]
    public void DeleteEmployee(Employee employee)
            { . . . }

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

    Ключевые термины

    Службы RIA Services, шаблон Silverlight Business Application, авторизация, аутентификация, регистрация пользователя, локализация приложения, EDM-модель, группировка данных, фильтрация данных, дочернее окно Silverlight, валидация, роль пользователя

    Windows Communication Foundation, WCF, Rich Internet application, RIA, RIA Services, Silverlight Business Application, Domain Service Class, ObjectContext, BusyIndicator, PageSize, AutoCompleteBox, ChildWindow, Authentication, webContext.

    Краткие итоги

    Рассмотрены вопросы построения приложения Silverlight на базе шаблона Business Application. При проектировании бизнес-приложения проанализированы подходы к локализации, созданию EDM-модели данных и службы WCF RIA Services, построению пользовательского интерфейса с использованием источников данных и автоматической генерации представления данных, вопросы сортировки и фильтрации информации в пользовательских интерфейсных элементах, реализацию функция модификации, вставки, удаления и валидации данных, аутентификацию и авторизацию пользователей.

    Ресурсы для углубленного изучения

  • Опубликованы новые материалы по Silverlight и RIA // http://blogs.msdn.com/b/rudevnews/archive/2010/10/10/silverlight-ria.aspx
  • WCF RIA Services // http://msdn.microsoft.com/ru-ru/library/ee707344(en-us,VS.91).aspx.
  • Microsoft Silverlight // http://silverlight.net
  • Silverlight Toolkit // http://silverlight.codeplex.com/
  • Вопросы для самопроверки

  • Поясните назначение служб WCF RIA Services.
  • Как организована аутентификация пользователя с бизнес-приложения Silverlight?
  • Как реализуется локализация бизнес-приложения Silverlight?
  • Зачем класс DomainService помечается атрибутом [EnableClientAccess( )]?
  • Для чего используется атрибут Display в файле метаданных?
  • Для чего используется атрибут Include в файле метаданных?
  • Поясните назначение класса BusyIndicator.
  • Поясните назначение класса PageSize.
  • Как реализуется группировка в источнике данных?
  • Как реализуется фильтрация в источнике данных?
  • Поясните функциональные возможности элемента управления AutoCompleteBox.
  • Поясните назначение валидации данных.
  • Как можно реализовать валидацию в бизнес-приложения Silverlight?
  • Какие способы аутентификации предусмотрены в бизнес-приложения Silverlight?
  • Для чего можно использовать роли в бизнес-приложения Silverlight?
  • Заключение

    При написании данного курса ставилась цель представить основные возможности технологии WPF и Silverlight для разработки приложений. Многие вопросы не вошли в данное пособие. Это прежде всего использование средств разработки Expression Design и Expression Blend, работа со звуком и видео, анимация, деловая графика, вопросы интеграции приложений с другими системами, например SharePoint Services.

    Технологии Microsoft постоянно развиваются, так на момент завершения работы над пособием вышли пакет обновлений Visual Studio 2010 SP1, новая версия библиотеки WCF Data Services , которая предназначена для поддержки открытого стандарта обмена данными OData на платформе .NET, библиотека WCF RIA Services V1.0 SP1 для Silverlight 4, которая упрощает традиционный n-уровневый шаблон приложения путем согласования платформ ASP.NET и Silverlight.

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

    Замечания, пожеления и ваше мнение по данному пособию можно направлять по адресу alexdoljenko@mail.ru.

    Страницы:

    Цель

    Освоить основы технологии разработки бизнес-приложение Silverlight на основе WCF RIA Services.

    Общие сведения о насыщенных Silverlight-приложениях

    При изучении лекций 6 и 7 построение Silverlight-приложения осуществлялось с реализацией слоя бизнес-логики, в основном, на стороне клиента. В корпоративных приложениях возникает необходимость переноса основной бизнес-логики или её части на сервер. Это диктуется требованиями обеспечения эффективной эксплуатации и сопровождения информационных систем.

    Для решения таких задач Microsoft предлагает службы Windows Communication Foundation (WCF) RIA Services. RIA (Rich Internet Application) — это приложение, доступное через интернет, с богатой функциональностью традиционных настольных приложений [].

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

    В Visual Studio 2010 имеются шаблоны для создания Silverlight бизнес-приложений с поддержкой служб RIA Services, которые включают реализацию многих функций, характерных для корпоративных приложений, таких как элементы управления для аутентификации, авторизации и регистрации пользователей, механизмы навигации, с помощью которых можно расширять функциональность приложения, добавляя новые страницы Silverlight.

    Исследуем возможности построения корпоративных приложений на основе шаблона бизнес-приложений и служб WCF RIA Services при конструировании Silverlight-приложения.

    Создание проекта Silverlight бизнес-приложения.

    Создадим новый проект, используя шаблон Silverlight Business Application, задав для него имя ).

    (рис 10.1) Страница создания проекта

    Инструментальная среда Visual Studio 2010 в соответствии с выбранным шаблоном генерирует новое решение, состоящее из двух проектов (рис 10.2).

    (рис 10.2) Главная страница проекта EnterpriceBusinessApplication

    В сгенерированном решении имеются два проекта: клиентский Silverlight-проект EnterpriceBusinessApplication и серверный Web-проект EnterpriceBusinessApplication.Web. Каждый проект структурирован, включает папки и отдельные файлы, назначение которых будет рассмотрено позднее (рис 10.3)

    (рис 10.3) Состав решения Enterprice BusinessApplication

    При запуске проекта на экран выводится главная страница приложения (рис 10.4).

    (рис 10.4) Главная страница приложения

    На главной странице имеются три гиперссылки: Home – переход на главную страницу, About – переход на страницу с данными о программе, которые должен подготовить разработчик, login – для перехода на окно авторизации пользователя.

    Для аутентификации пользователя предлагается форма авторизации (рис 10.5), а для регистрации нового пользователя – окно регистрации (рис 10.6).

    Как видно из рис. 8.4 – 8.6 все надписи на страницах приложения приведены на английском языке, что является неудобными для пользователей России. Этот недостаток созданного приложения можно исправить путем замены в коде соответствующих английских строк на русские. Однако в шаблоне бизнес-приложения Silverlight предусмотрен более изящный способ локализации приложения на разные языки с помощью ресурсов. Так, например свойство Text для текстового блока HeaderText задается в расширении разметки ссылкой на статический ресурс.

    <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}" 
    	Text="{Binding Path=ApplicationStrings.HomePageTitle, 
    	Source={StaticResource ResourceWrapper}}"/>
    (рис 10.6) Авторизация пользователя(рис 10.5) Регистрация пользователя

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

    Для локализации приложения вначале необходимо отсоединить клиентскую часть – проект ).

    (рис 10.7) Выгрузка клиентской части проекта

    Для редактирования XAML-описания отсоединенного проекта в контекстном меню выберем пункт ).

    (рис 10.8) Редактирование проекта

    В XAML-описании проекта для элемента SupportedCultures добавим код для культуры русского языка ru-RU.

    <SupportedCultures>en-US,ru-RU</SupportedCultures>

    После сохранения изменений требуется перезагрузить проект (рис 10.9).

    (рис 10.9) Перезагрузка проекта

    Для клиентского проекта а). Копию ресурса следует переименовать в б).

    (рис 10.10) Файл ресурса строк приложения

    Двойным щелчком на файле ресурса .

    (рис 10.11) Ресурс строк приложения с английскими значениями

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

    (рис 10.12) Ресурс строк приложения с русскими значениями строк

    Аналогичную последовательность действий по копированию, переименованию и переводу значений строк необходимо выполнить для файлов ресурсов клиентского приложения ErrorResources.resx и SecurityQuestions.resx, а также файлов RegistrationDataResources.resx и ValidationErrorResources.resx серверного проекта EnterpriceBusinessApplication.Web.csproj.

    Для серверной части проекта необходимо внести изменения в файл EnterpriceBusinessApplicationTestPage.aspx, добавив в головной элемент XAML-описания атрибуты UICulture и Culture.

    <%@ Page Language="C#" AutoEventWireup="true" UICulture="auto" Culture="auto"%>

    Измененный файл EnterpriceBusinessApplicationTestPage.aspx необходимо сохранить.

    Для настройки приложения на русский язык достаточно в свойствах Internet Explorer установить первым в списке языков русский язык (рис 10.13).

    (рис 10.13) Установка языка для Internet Explorer

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

    (рис 10.15) Главная страница приложения(рис 10.14) Авторизация пользователя(рис 10.16) Регистрация пользователя

    Создание модели и сервисов данных

    Создание EDM-модели данных рассматривалось в лекциях 6 и 7. Исходной базой данных для EDM-модели модели является существующая база данных сотрудников организации – PersonalEnterprice. В Web-проект добавим папку ).

    (рис 10.17) Модель данных

    Таблица .

    Свойства таблицы Employee
    Свойство Назначение
    ID Суррогатный ключ
    FirstName Имя
    SecondName Отчество
    LastName Фамилия
    TitleID Внешний ключ для таблицы Title
    Role Роль/группа
    Email Адрес электронной почты
    Phone Телефон
    Inn ИНН
    FirstData Дата приема на работу
    LastDate Дата увольнения
    Picture Фотография
    Title Связь с таблицей Title
    Свойства таблицы Title
    Свойство Назначение
    ID Суррогатный ключ
    Title1 Должность
    Employees Связь с таблицей Employee

    При генерации EDM-модели создается класс PersonalEnterpriceEntities, который наследуется от класса ObjectContext. Класс PersonalEnterpriceEntities содержит описание сущностей базы данных PersonalEnterprice как свойств, метод добавления сущностей и навигации. После создания EDM-модели необходимо перекомпилировать проект.

    Следующим шагом проектирования приложения является создание службы WCF RIA Services. В Web проекте создадим папку ).

    (рис 10.18) Добавление Domain Service Class

    При добавлении сервиса WCF RIA Services необходимо в окне ).

    (рис 10.19) Настройка добавления Domain Service Class

    В результате добавления EDM-модели и сервиса WCF RIA Services в проекте будут сформированы в папке ).

    (рис 10.20) Обозреватель решений проекта. Серверная часть

    Созданный серверный класс EmployeeDomainService является, в общем случае, потомком класса DomainService, в нашем случае – это потомок класса LinqToEntitiesDomainService<PersonalEnterpriceEntities>, который обеспечивает доступ к данным на стороне сервера. Код класса EmployeeDomainService приведен в приложении.

    Класс EmployeeDomainService помечен атрибутом [ EnableClientAccess() ], который обеспечивает доступ к сервису со стороны клиента. Методы GetEmployees() и GetTitles() класса предназначены для загрузки в приложение данных из базы, а методы Insert(), Update() и Delete() – для манипулирования данными.

    Класс EmployeeDomainService.metadata.cs содержим метаданные о сущностях модели данных. Код класса EmployeeMetadata приведен в приложении.

    Для разрабатываемого приложения необходима связь от таблицы Employee к таблице Title, поэтому в класс EmployeeMetadata необходимо добавить коллекцию Titles.

    EntityCollection<Title> Titles { get; set; }

    Метаданные используются, в частности, для генерации элементов контроля в клиентской части приложения и валидации данных. Свойства данных задаются с помощью атрибутов пространства имен System.ComponentModel.DataAnnotations. Если свойства не помечать специальными атрибутами, то в элементах контроля им будут присваиваться метки в соответствии с именами свойств, например FirstName, а порядок формирования элементов, например в элементе контроля DataGrid, будет соответствовать порядку данных в таблице и соответственно для сущности данных. Так, если для свойства Picture необходимо задать метку Фото и в списке свойств в таблице поместить её на первое место, то необходимо для атрибута Display определить свойства Name и Order.

    [Display(Name = "Фото", Order = 0)]
    public byte[ ] Picture { get; set; }

    Перечень свойств атрибута Display приведен в табл.8.3

    Свойства атрибута Display
    Имя Описание
    AutoGenerateField Получает или задает значение, указывающее, нужно ли для отображения этого поля автоматически создавать пользовательский интерфейс.
    AutoGenerateFilter Получает или задает значение, которое используется для фильтрации.
    Description Получает или задает значение, которое используется для отображения описания пользовательского интерфейса.
    GroupName Получает или задает значение, используемое для группировки полей в пользовательском интерфейсе.
    Name Получает или задает значение, которое используется для отображения в элементе пользовательского интерфейса.
    Order Получает или задает порядковый вес столбца.
    Prompt Получает или задает значение, которое будет использоваться для задания подсказки в элементе пользовательского интерфейса.
    ResourceType Получает или задает тип, содержащий ресурсы для свойств ShortName, Name, Prompt и Description.
    ShortName Получает или задает значение, используемое в качестве метки столбца сетки.
    TypeId При реализации в производном классе возвращает уникальный идентификатор для этого Attribute.

    Для свойств класса EmployeeMetadata необходимо указать атрибут [ Display ], который будет использованы в интерфейсных элементах. Для атрибута Titles дополнительно укажем атрибут [ Include ], который предписывает сформировать коллекцию объектов Title в соответствии со свойствами навигации. Код модифицированного класса EmployeeMetadata приведен в приложении.

    При построении сервиса WCF RIA Services на клиенте автоматически генерируется файл ).

    (рис 10.21) Обозреватель решений проекта. Клиентская часть

    В файле EnterpriceBusinessApplication.Web.g.cs, в частности, имеются классы сущностей, аналогичные классам серверной части приложения и класс EmployeeDomainContext, который является наследником класса DomainContext и обеспечивает доступ к данным и методам сервиса со стороны клиента.

    Разработка клиентской части приложения

    Разработаем страницу для отображения списка сотрудников. Добавим в клиентскую часть, в папку ).

    (рис 10.22) Вкладка Источник данных

    Перетащим источник данных для таблицы .

    (рис 10.23) Конструктор страницы EmployeePage

    Добавим на главной странице Home гиперссылку на созданную страницу EmployeePage, указав для контента расширение разметки для ссылки на строку ресурса ApplicationStrings.EmployeePageTitle.

    <HyperlinkButton x:Name="Link3" Style="{StaticResource LinkStyle}"
    	NavigateUri="/EmployeePage" TargetName="ContentFrame" 
    	Content="{Binding Path=ApplicationStrings.EmployeePageTitle, 
    	Source={StaticResource ResourceWrapper}}"/>

    После компиляции и запуска приложения страница .

    (рис 10.24) Страница EmployeePage – первоначальный вариант

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

    Добавим в папку Helpers клиентского приложения класс конвертора ImageConverter. Аналогичный конвертор использовался в лекции 7.

    Добавим в ресурсы приложения ресурс конвертора с ключом ImageConverter.

    <Application.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="Assets/Styles.xaml"/> 
                    <ResourceDictionary>
                        <app:ResourceWrapper x:Key="ResourceWrapper" />
                        <app:NotOperatorValueConverter x:Key="NotOperatorValueConverter" />
                        <app:ImageConverter x:Key="ImageConverter" />
                    </ResourceDictionary>
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>                
        </Application.Resources>

    Изменим XAML-описание для столбца Фото сетки employeeDataGrid.

    <sdk:DataGridTemplateColumn x:Name="pictureColumn" Header="Фото" Width="SizeToHeader">
      <sdk:DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
           <Image Source="{Binding Path=Picture, 
                      Converter={StaticResource ImageConverter}}" 
                      Margin="1" Stretch="UniformToFill" Height="60" />
        </DataTemplate>
      </sdk:DataGridTemplateColumn.CellTemplate>
    </sdk:DataGridTemplateColumn>

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

    Для отображения должности сотрудника в сетке employeeDataGrid на странице EmployeePage необходимо при загрузке сущности Employee обеспечить загрузку связанных с ней данных из таблицы Title. Это можно сделать модифицировав метод GetEmployees() класса сервиса данных EmployeeDomainService.

    public IQueryable<Employee> GetEmployees()
    {
        return this.ObjectContext.Employees.Include("Title").OrderBy(e => e.LastName);
    }

    В методе GetEmployees() используется класс ObjectContext, который предоставляет возможность выполнять запросы и работать с данными сущности как с объектами. Данный класс обеспечивает взаимодействие с данными как с объектами, которые являются экземплярами типов сущности, определенными в модели EDM.

    Метод GetEmployees() возвращает экземпляр класса IQueryable, который представляет типизированный запрос по отношению к концептуальной модели в данном контексте объекта. Класс имеет ряд методов для формирования запросов к модели данных, некоторые из которых приведены в табл. 8.4.

    Методы класса ObjectContext
    Имя Описание
    GroupBy Группирует результаты запроса по заданным критериям.
    Include Задает связанные объекты, включаемые в результаты запроса.
    Intersect Ограничивает результаты запроса, включая только те, которые вошли в результаты другого запроса объектов.
    OrderBy Сортирует результаты запроса по заданным критериям.
    Select Ограничивает результаты запроса только теми свойствами, которые определены в заданной проекции.
    Skip Сортирует результаты запроса по заданным критериям и пропускает указанное число результатов.
    Top Ограничивает результаты запроса указанным числом элементов.
    Where Ограничивает запрос результатами, соответствующими определенным критериям фильтрации.

    В методе GetEmployees() для сущности Employees используется метод Include() для объединение в запросе сущностей Employees и Titles, а также метод OrderBy() для сортировки результатов запроса по фамилии ( e => e.LastName ).

    В XAML-описании колонки Должность сетки employeeDataGrid страницы EmployeePage следует изменить путь привязки, чтобы получить данные о должности ( Title1 ) из сущности Title.

    <sdk:DataGridTextColumn x:Name="titleColumn" 
    	Binding="{Binding Path=Title.Title1}" 
    	Header="Должность" Width="150" />

    После компиляции и запуска программы страница .

    (рис 10.25) Страница Сотрудники

    При загрузке страницы Сотрудники приложения происходит загрузка данных из базы данных, что требует определенного времени. Во время загрузки данных на экране дисплея появляется пустая страница и не понятно работает или нет приложение. Устранение такой неопределенности можно реализовать с помощью класса BusyIndicator, построенного в соответствии с шаблоном проекта. Использование данного класса обеспечивает вывод элемента управления ProgressBar на экран во время выполнения длительной операции.

    Объект BusyIndicator – индикатор длительно выполняемой операции является контейнером, в который необходимо поместить объект, куда должны быть выведены данные в результате выполнения операции. Для разрабатываемого приложения в контейнер BusyIndicatorLoadData поместим сетку employeeDataGrid, которая визуализирует данные о сотрудниках.

    <local:BusyIndicator x:Name="BusyIndicatorLoadData"
    	IsBusy="{Binding IsLoadingData, ElementName= employeeDomainDataSource}"
    	BusyContent="{Binding Path=ApplicationStrings.BusyIndicatorLoadData, 
    	Source={StaticResource ResourceWrapper}}" >
    	<sdk:DataGrid  Name="employeeDataGrid" . . .>
    	....
    	</sdk:DataGrid.Columns>
    </local:BusyIndicator>

    Свойство IsBusy объекта BusyIndicator задает состояние отображения элемента управления ProgressBar. Данное свойство с помощью расширения разметки привязывается к свойству IsLoadingData объекта DomainDataSource – employeeDomainDataSource. Свойство BusyContent определяет строку, которая будет выводиться в элементе управления ProgressBar при его отображении. В расширении разметки для свойства BusyContent используется задание пути ( ApplicationStrings.BusyIndicatorLoadData ) к статическому ресурсу. При загрузке данных на экране будет отображаться элемент управления ).

    (рис 10.26) Индикация загрузки данных

    При разработке дизайна страницы приложения имеются много вариантов визуализации данных. Например, можно реализовать организацию разбиения и перелистывания страниц с использованием объекта DataPager. Добавим в XAML-описание страницы EmployeePage объект DataPager после описания индикатора загрузки данных BusyIndicatorLoadData.

    <sdk:DataPager Height="26" HorizontalAlignment="Left" 
    	VerticalAlignment="Bottom" Margin="25,0,0,35" 
    	Name="dataPager1"  Width="200" 
    	Source="{Binding ElementName=employeeDomainDataSource, Path=Data}" 
    	PageSize="5" />

    Источником данных ( ).

    (рис 10.27) Реализация перелистывания страниц элементом PageSize

    Еще одним возможным вариантом визуализации данных в DataGrid является использование группировки по определенному полю. Предположим, что требуется провести группировку по должности сотрудников. Для этого необходимо в XAML-описание объекта DomainDataSource добавить свойство GroupDescriptors, указав для атрибута PropertyPath, который определяет свойство группировки, значение поля, по которому будет проводиться группировка – Title.Title1.

    <riaControls:DomainDataSource AutoLoad="True" 
    	d:DesignData="{d:DesignInstance my:Employee, CreateList=true}" 
    	Height="0" Width="0"
    	LoadedData="employeeDomainDataSource_LoadedData"
    	Name="employeeDomainDataSource"
    	QueryName="GetEmployeesQuery" >
    	<riaControls:DomainDataSource.DomainContext>
    		<my1:EmployeeDomainContext />
    	</riaControls:DomainDataSource.DomainContext>
    	<riaControls:DomainDataSource.GroupDescriptors>
    		<riaControls:GroupDescriptor PropertyPath="Title.Title1"/>
    	</riaControls:DomainDataSource.GroupDescriptors>
    </riaControls:DomainDataSource>

    На рис 10.28 приведены результаты работы приложения с группировкой сотрудников по должности. В списке сотрудников для каждой должности указывается количество записей в группе, группу можно свернуть и развернуть. Так на рис 10.28 развернутой является только группа, соответствующая должности "Начальник клиентского отдела".

    (рис 10.28) Реализация группировки на странице Сотрудники

    Следующим этапом разработки приложения является организация поиска/фильтрации данных по фамилии сотрудников и по должности.

    Вначале модифицируем макет страницы EmployeePage. Добавим в сетку LayoutRoot две строки и три столбца.

    Контейнер BusyIndicatorLoadData поместим во второй строке, сетки LayoutRoot, объединив для этого три столбца.

    <local:BusyIndicator x:Name="BusyIndicatorLoadData" 
    	Grid.Row="1" Grid.ColumnSpan="3" ...>

    В первой строке и колонки LayoutRoot разметим объект TextBlock с текстом "Список сотрудников".

    Во второй колонке первой строки LayoutRoot поместим элементы управления необходимые для задания параметров фильтрации данных: текстовые блоки с текстами "Поиск", "По фамилии" и "По должности", блок для ввода фамилии textBoxSurname и элемент контроля AutoCompleteBox для ввода должности. Все перечисленные элементы контроля скомпонованы в сетке gridSearch, которая помещена в рамку borderSearch. XAML-описание фрагмента страницы Сотрудники сетки gridSearch, визуализирующей элементы контроля для поиска данных по сотрудникам, приведены в приложении.

    Для фильтрации данных по фамилии сотрудника в XAML-описание объекта DomainDataSource необходимо добавить объект FilterDescriptors, который представляет описание фильтра для операций запроса в DomainDataSource объекте.

    <riaControls:DomainDataSource.FilterDescriptors>
    	<riaControls:FilterDescriptor Operator="StartsWith" 
    		PropertyPath="LastName" 
    		Value="{Binding ElementName= textBoxLastName, Path=Text}" />
    </riaControls:DomainDataSource.FilterDescriptors>

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

    Значения перечисления FilterDescriptors
    Значение оператора фильтрации Описание действий
    Contains Значение должно содержать значение фильтра.
    EndsWith Значение должно заканчиваться значением фильтра.
    IsContainedIn Значение данных должны содержаться в значении фильтра.
    IsEqualTo Значение должно быть равно значению фильтра.
    IsGreaterThan Значение должно быть больше, чем значение фильтра.
    IsGreaterThanOrEqualTo Значение должно быть больше или равно значению фильтра.
    IsLessThan Значение должно быть меньше значения фильтра.
    IsLessThanOrEqualTo Значение должно быть меньше или равно значению фильтра.
    IsNotEqualTo Значение должно отличаться от значения фильтра.
    StartsWith Значение должно начинаться с символов, заданных в фильтре.

    В созданном фильтре для фамилии сотрудника использован оперетор .

    (рис 10.29) Реализация фильтрации по фамилии

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

    В папку DomainModel добавим класс TitleEmployee для формирования данных о должности сотрудников, имеющихся в сущности Employee.

    public class TitleEmployee
    {
        [Key]
        public string Title { set; get; }
    }

    Свойство Title снабжено атрибутом [ Key ] для обеспечения возможности формирования полей новой сущность на базе этого класса.

    В класс сервиса данных EmployeeDomainService добавим метод GetTitlesEmployee() для получения списка должностей сотрудников.

    public IQueryable<TitleEmployee> GetTitlesEmployee()
    {
        return this.ObjectContext.Employees.Select(e => new TitleEmployee 
    	{ Title = e.Title.Title1 }).Distinct();
    }

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

    <riaControls:DomainDataSource AutoLoad="True" 
    	d:DesignData="{d:DesignInstance my1:TitleEmployee, CreateList=true}" Height="0"
    	LoadedData="TitleEmployeeDomainDataSource_LoadedData"  Name="titleEmployeeDomainDataSource"
    	QueryName="GetTitlesEmployeeQuery" Width="0">
    	<riaControls:DomainDataSource.DomainContext>
    		<my1:EmployeeDomainContext />
    	</riaControls:DomainDataSource.DomainContext>
    </riaControls:DomainDataSource>

    Источник данных titleEmployeeDomainDataSource формирует список должностей с помощью метода GetTitlesEmployeeQuery() домена сервиса данных.

    Для ввода должности при фильтрации формы используется элемент управления AutoCompleteBox, источником привязки которого служит объект данных titleEmployeeDomainDataSource, значение для данного элемента определяются полем Title.

    <input:AutoCompleteBox Grid.Row="2" Grid.Column="1" Name="FilterText"
    	HorizontalAlignment="Left"
    	ValueMemberBinding="{Binding Title}" 
    	ItemsSource="{Binding ElementName=titleEmployeeDomainDataSource, 
    				Path=Data}"
    	TextChanged ="FilterText_TextChanged"
    	Margin="20,2,20,2" Height="23" Width="300"/>

    Элемент управления AutoCompleteBox поддерживает формирование подсказки при вводе первых символов слова, если оно имеется в источнике данных.

    В заключении процесса проектирования функции фильтрации данных по должности необходимо в источнике данных employeeDomainDataSource добавить в объект FilterDescriptors XAML-описание фильтра.

    <riaControls:FilterDescriptor PropertyPath="Title.Title1" 
    	Operator="IsEqualTo" Value="{Binding ElementName=FilterText, Path=Text}" />

    Если в источнике данных указаны несколько объектов FilterDescriptor, то они могут работать по схеме "Или" ( Or ) и по схеме "И" ( And ). Режим объединения фильтров задается свойством FilterOperator источника данных и в проектируемом приложении ему задано значение Or.

    На рис 10.30 иллюстрируется функциональность элемента управления – результаты работы приложения при выполнении фильтрации по должности.

    (рис 10.31) Подсказка при вводе должности(рис 10.30) Результаты фильтрации по должности

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

    На странице EmployeePage создадим кнопки buttonNewEmployee, buttonEdit и buttonDelete для реализации выполнения соответствующих операций.

    Добавим в клиентскую часть приложения дочернее окно, с помощью которого будем редактировать и добавлять данные по сотрудникам. Для этого в помощнике ) и задать имя ).

    (рис 10.32) Создание дочернего окна

    В конструкторе окна ). Для атрибутов сущности

    (рис 10.33) Источники данных

    С помощью мыши перетащим сущность ).

    (рис 10.34) Дочернее окно EmployeeChildWindow

    В XAML-описание окна будет добавлен контекст данных для сущности Employee.

    <riaControls:DomainDataSource AutoLoad="True" 
    	d:DesignData="{d:DesignInstance my1:Employee, CreateList=true}" 
    	Name="employeeDomainDataSource" Height="0" Width="0"
    	LoadedData="employeeDomainDataSource_LoadedData" 
    	QueryName="GetEmployeesQuery">
    	<riaControls:DomainDataSource.DomainContext>
    		<my:EmployeeDomainContext />
    	</riaControls:DomainDataSource.DomainContext>
    </riaControls:DomainDataSource>

    Если проанализировать XAML-описание окна EmployeeChildWindow, то можно увидеть, что элемент Image для отображение фотографии требует корректировки, а элемент ComboBox не настроен правильно на источник данных. Для отображения фотографии в привязке необходимо использовать конвертор. В дополнение к этому поместим объект Image в рамку Border.

    <Border Grid.Column="2" Name="Border"
    	Padding="5" Width="200" Height="250" CornerRadius="10" 
    	Background="#FFC8E5E9" Margin="3" VerticalAlignment="Center"
    	ToolTipService.ToolTip=
    	"Для изменения фотографии щелкните на ней правой кнопкой мыши" 
    	MouseRightButtonDown="Border_MouseRightButtonDown" >
    	<Image  Height="240" HorizontalAlignment="Center" Margin="3" 
    		Name="pictureImage" 
    		Source="{Binding Path=Picture, Converter={StaticResource ImageConverter}}" 
    			Stretch="UniformToFill" VerticalAlignment="Center" Width="196" />
    </Border>

    Для подготовки источника данных для comboBoxTitle необходимо в XAML-описание окна EmployeeChildWindow добавить контекст данных для сущности Title.

    <riaControls:DomainDataSource AutoLoad="True" 
    	d:DesignData="{d:DesignInstance my1:Title, CreateList=true}" 
    	LoadedData="titleDomainDataSource_LoadedData" 
    	Name="titleDomainDataSource" 
    	QueryName="GetTitlesQuery">
    	<riaControls:DomainDataSource.DomainContext>
    		<my:EmployeeDomainContext />
    	</riaControls:DomainDataSource.DomainContext>
    </riaControls:DomainDataSource>

    После этого можно отредактировать описание для выпадающего списка должностей.

    <ComboBox x:Name="comboBoxTitle" Height="23" Grid.Column="2" Grid.Row="4" 
    	HorizontalAlignment="Left" 
    	ItemsSource="{Binding ElementName=titleDomainDataSource, Path=Data}"
    	DisplayMemberPath="Title1" 
    	SelectedValuePath="ID" 
    	SelectedValue="{Binding Path=TitleID,  Mode=TwoWay,
    				  UpdateSourceTrigger=Default}"  
    	Margin="3,3,0,0" VerticalAlignment="Top" Width="198">
    </ComboBox>

    Теперь следует создать вызов дочернего окна из страницы EmployeePage при нажатии кнопки buttonEdit.

    private void buttonEdit_Click(object sender, RoutedEventArgs e)
    {
    	Employee employeeEdit = employeeDataGrid.SelectedItem as Employee;
    	if (employeeEdit != null)
    	{
    	EmployeeChildWindow winEmpEdit = new EmployeeChildWindow(employeeEdit);
    	winEmpEdit.Closed += new EventHandler(winEmpEdit_Closed);
    	winEmpEdit.Title = "Редактирование данных";
    	winEmpEdit.Show();
    	}
    }

    В обработчике нажатия кнопки buttonEdit создается экземпляр employeeEdit сущности Employee на основе выделенной в текущий момент строки сетки данных employeeDataGrid.

    Employee employeeEdit = employeeDataGrid.SelectedItem as Employee;

    Если объект employeeEdit существует, то создается экземпляр winEmpEdit дочернего окна EmployeeChildWindow, которому в качестве параметра передается экземпляр сущности данных employeeEdit.

    EmployeeChildWindow winEmpEdit = new EmployeeChildWindow(employeeEdit);

    Затем включают прослушивания события закрытия окна.

    winEmpEdit.Closed += new EventHandler(winEmpEdit_Closed);

    Для дочернего формируется заголовок и окно выводится на экран дисплея.

    winEmpEdit.Title = "Редактирование данных";
    winEmpEdit.Show();

    Обработка события закрытия дочернего окра реализуется методом winEmpEdit_Closed(), в котором по результату нажатия кнопки ОК проводят сохранение изменений в базе данных

    employeeDomainDataSource.SubmitChanges();

    а при нажатии кнопки Отмена – отмена проведенного редактирования данных

    employeeDomainDataSource.RejectChanges();

    Полный код обработчика winEmpEdit_Closed приводится ниже.

    void winEmpEdit_Closed(object sender, EventArgs e)
    {
        EmployeeChildWindow emp = (EmployeeChildWindow)sender;
        if (emp.Employee != null)
        {
            if (emp.DialogResult == true)
            {
                employeeDomainDataSource.SubmitChanges();
            }
            else
            {
                employeeDomainDataSource.RejectChanges();
            }
        }
    }

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

    (рис 10.35) Редактирование данных

    Текстовые поля редактируются непосредственно в поле, должность выбирается из выпадающего списка, даты задаются с помощью выпадающего календаря, а при наведении курсора на фотографию выводится подсказка "Для изменения фотографии щелкните на ней правой кнопкой мыши" и для корректировки фотографии по нажатию правой кнопки мыши выводится окно "Открыть" для задания нового файла фотографии. Для завершения редактирования нажимается кнопка ОК, а для отмены редактирования – кнопка Отмена.

    Формирование данных по новому сотруднику будем выполнять с использованием созданного ранее дочернего окна EditEmployeeChildWindow. Для того, чтобы в коде можно было различать режимы в котором находится дочернее окно будем использовать свойство Tag окна. Так при формировании окна в режиме редактирования свойству Tag присвоим значение edit, а при добавлении нового сотрудника – new.

    private void buttonNew_Click(object sender, RoutedEventArgs e)
            {
                Employee  newEmployee= new Employee();
                newEmployee.Role= "guest";
                newEmployee.LastName= "не задано";
                newEmployee.FirstName = "не задано";
                newEmployee.SecondName = "не задано";
                newEmployee.Email = "a@my.com";
                newEmployee.Phone = "(999) 999-9999";
                newEmployee.Inn = "999999999999";
                newEmployee.TitleID = 1;
                newEmployee.FirstDate = DateTime.Now;
                EmployeeChildWindow winEmpEdit = new 
    			EmployeeChildWindow(newEmployee);
                winEmpEdit.Title = "Регистрация нового сотрудника";
                winEmpEdit.Tag = "new";
                winEmpEdit.Closed += new EventHandler(winEmpEdit_Closed);
                winEmpEdit.Show();
            }

    В методе buttonNew_Click() создается экземпляр класса Employee, задаются его поля, затем по аналогии с режимом редактирования данных, создается экземпляр дочернего окна EmployeeChildWindow, задаются его свойства, включается прослушивание события закрытия окна и оно выводится на экран.

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

    void winEmpEdit_Closed(object sender, EventArgs e)
            {
                EmployeeChildWindow emp = (EmployeeChildWindow)sender;
                switch (emp.Tag.ToString())
                {
                    case "edit":
                        if (emp.Employee != null)
                        {
                            if (emp.DialogResult == true)
                            {
                                employeeDomainDataSource.SubmitChanges();
                            }
                            else
                            {
                                employeeDomainDataSource.RejectChanges();
                            }
                        }
                        break;
                    case "new":
                        if (emp.Employee != null)
                        {
                            if (emp.DialogResult == true)
                            {
                                EmployeeDomainContext employeeDomain = 
    (employeeDomainDataSource.DomainContext) as EmployeeDomainContext;
                                employeeDomain.Employees.Add(emp.Employee);
                                employeeDomainDataSource.SubmitChanges();
                            }
                         }
                        break;
                }
            }

    При создании новой записи по сотруднику используется метод Add сущности контекста домена службы DomainService и изменения в источнике данных фиксируются методом SubmitChanges().

    При работе приложения и нажатии кнопки ).

    (рис 10.36) Добавление нового сотрудника

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

    Удаление записей по сотруднику реализуются в приложении методом buttonDelete_Click(), который является обработчиком события Click кнопки buttonDelete.

    private void buttonDelete_Click(object sender, RoutedEventArgs e)
            {
                Employee emp = (Employee)(employeeDataGrid.SelectedItem);
                if (emp != null)
                {
                    MessageBoxResult result = MessageBox.Show("Удалить сотрудника: \n" +
                                                emp.LastName+"  " + emp.FirstName + "  "+  emp.SecondName,
                      "Предупреждение", MessageBoxButton.OKCancel);
                    if (result == MessageBoxResult.OK)
                    {
                        EmployeeDomainContext employeeDomain = 
                                            (employeeDomainDataSource.DomainContext) as 
    						EmployeeDomainContext;
                        employeeDomain.Employees.Remove(emp);
                        employeeDomainDataSource.SubmitChanges();
                    }
                }
            }

    Удаление данных из источника реализуется методом ).

    (рис 10.37) Удаление данных по сотруднику

    Валидация ввода данных

    Вопросы проверки правильности введенных данных, то есть валидации рассматривались в лекциях 3 и 4, в контексте привязки данных на уровне представления. Реализация такого варианта валидации осуществлялась на стороне клиента. Проверка правильности введенных данных относится к бизнес-логике приложения и его по возможности следует отделять от уровня представления данных. В корпоративных приложениях на базе сервисов WCF RIA Services имеется возможность реализации валидации на серверной стороне. Для этого используются атрибуты метаданных объектов из пространства имен System.ComponentModel.DataAnnotations. Мы уже рассматривали атрибут [ Display ] для задания представления имен свойств сущности Employee в файле метаданных EmployeeMetadata. Для валидации по умолчанию используются следующие атрибуты:

  • DataTypeAttribute – определяет тип для проверяемого свойства;
  • RangeAttribute – фиксирует диапазон возможных значений;
  • RegularExpressionAttribute – задает регулярное выражение для строковых данных;
  • RequiredAttribute – определяет обязательность свойства при вводе;
  • StringLengthAttribute – задает длину строки.
  • Валидацию свойства FirstName сущности Employee проведем для выполнения следующих условий:

  • вводимое имя может содержать только строчные и прописные буквы русского алфавита и символ пробела, для всех 20-ти возможных для ввода символов. При нарушении этого условия необходимо вывести сообщение "Недопустимые символы или более 20 символов";
  • свойство FirstName является обязательным для ввода и при нарушении этого условия вывести сообщение "Поле обязательно должно быть заполнено".
  • Фрагмент кода класса EmployeeMetadata в части описания свойства FirstName приведен ниже.

    [Display(Name = "Имя", Order = 2)]
    [RegularExpression((@"^[А-Яа-я' ']{1,20}$"),
                                 ErrorMessage = "Недопустимые символы или более 20 символов")]
    [Required(ErrorMessage = "Поле обязательно должно быть заполнено")]
    public string FirstName { get; set; }

    Аналогичные атрибуты установим в классе EmployeeMetadata для свойств LastName, SecondName и для свойства Role, за исключением атрибута [ Required ].

    Для свойства Email с помощью регулярного выражения будем проверять требуемый формат для электронной почты, а сообщение об ошибке должно формироваться ссылкой на ресурс.

    [Display(Name = "Эл. почта", Order = 7)]
    [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; }

    Для свойства Inn определим допустимость ввода только цифровых символов, которых может быть 12.

    [Display(Name = "ИНН", Order = 6)]
    [RegularExpression((@"^[0-9]{1,12}$"), 
    ErrorMessage = "Допустимы толька цифровые символы, длина ИНН - 12 символов")]
    [CustomValidation(typeof(ValidaterInn), "ValidateInn")]
    public string Inn { get; set; }

    Для свойства Inn применен атрибут CustomValidation, который позволяет задать пользовательское правило проверки. В параметрах атрибута CustomValidation определен класс ValidaterInn, определяющий правило проверки, и метод проверки – ValidateInn. Класс ValidaterInn создадим в папке Models/ Shared проекта EnterpriceBusinessApplication.Web.

    public class ValidaterInn
    {
        public static ValidationResult ValidateInn(string Inn)
        {
            ValidationResult res;
            if (Inn.Length == 12)
            {
                res = ValidationResult.Success;
            }
            else
            {
                res = new ValidationResult("ИНН должен содержать 12 символов");
            }
            return res;
        }
    }

    Статический метод .shared, то есть ValidaterInn.shared.cs.

    Код измененного класса EmployeeMetadata приведен в приложении.

    На рис 10.39, рис 10.40 приведены результаты тестирования процесса валидации приложения.

    (рис 10.40) Проверка ввода фамилии(рис 10.39) Проверка ввода адреса электронной почты и ИНН

    Аутентификация и авторизация пользователей

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

    Для настройки Windows- авторизации в проекте EnterpriceBusinessApplication.Web для файла Web.config необходимо свойству mode объекта authentication установить значение Windows.

    <authentication mode="Windows"/ >

    В клиентском проекте EnterpriceBusinessApplication в конструкторе класса App для свойства аутентификации Authentication веб конекста webContext присвоить значение вновь созданного экземпляра класса Windows аутентификации – WindowsAuthentication.

    public App()
    {
    	InitializeComponent();
    	WebContext webContext = new WebContext();
    	webContext.Authentication = new WindowsAuthentication();
    	this.ApplicationLifetimeObjects.Add(webContext);
    }

    После загрузки приложения происходит автоматически Windows-авторизация пользователя (рис 10.41).

    (рис 10.41) Windows-авторизация

    Для аутентификации зарегистрированных в приложении пользователей в проекте EnterpriceBusinessApplication.Web для файла Web.config необходимо свойству mode объекта authentication установить значение Forms и задать имя форы.

    <authentication mode="Forms">
          <forms name=".EnterpriceBusinessApplication_ASPXAUTH" />
    </authentication>

    В конструкторе клиентского класса App следует для свойства аутентификации Authentication веб контекста webContext создать экземпляр класса пользовательской авторизации FormsAuthentication.

    public App()
    {
    	InitializeComponent();
    	WebContext webContext = new WebContext();
    	webContext.Authentication = new FormsAuthentication();
    	this.ApplicationLifetimeObjects.Add(webContext);
    }

    Нового пользователя при открытии приложения необходимо зарегистрировать (рис 10.42). Зарегистрированный пользователь должен пройти аутентификацию (рис 10.43). Если аутентификация прошла успешно, то на странице приложения выводится имя пользователя, работающего с приложением (рис 10.44).

    (рис 10.43) Регистрация нового пользователя(рис 10.42) Аутентификация пользователя(рис 10.44) Вход в приложение зарегистрированного пользователя

    Если имя пользователя или пароль введены с ошибкой, то аутентификация пользователя не проходит (рис 10.45).

    (рис 10.45) Отказ в аутентификации пользователя

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

    Таким образом, аутентификация осуществляется на этапе регистрации пользователя, а авторизация реализуется на базе аутентификации с применением механизма ролей.

    Для создания ролей необходимо настроить конфигурацию ASP.NET приложения. Для проекта ).

    (рис 10.46) Выбор настройки конфигурации ASP.NET

    На странице администрирования веб-узла (рис 10.47) следует открыть вкладку ).

    (рис 10.48) Страница администрирования веб-узла(рис 10.47) Вкладка безопасность страницы администрирования веб-узла

    Выбрав ссылку "Создание ролей и управления ими" перейдем на страницу, где создадим две новые роли: администратор и оператор (рис 10.49).

    (рис 10.49) Создание ролей приложения

    Для создания новых пользователей выберем ссылку "Создать пользователя" и создадим два новых пользователя – одного с ролью Адмитистратор, а другого – "Оператор" (рис 10.50).

    (рис 10.50) Создание нового пользователя с заданием роли

    На странице администрирования веб-узла (рис 10.48) предоставляются возможности создания и управления пользователями, ролями и правилами доступа.

    Для разграничения прав доступа различных пользователей к приложению используются атрибуты для классов и методов службы домена. Если ставится задача ограничить доступ к таблице Employee только зарегистрированным пользователям, метод загрузки данных GetEmployees() для этой таблицы класса EmployeeDomainService должен быть отмечен атрибутом RequiresAuthentication().

    [RequiresAuthentication()]
    public IQueryable<Employee> GetEmployees()

    При переходе на страницу ).

    (рис 10.51) Отказ в доступе для незарегистрированного пользователя

    Если переход на страницу ).

    (рис 10.52) Разрешен доступ для зарегистрированного пользователя

    Если требуется разграничить роли пользователей при выполнении определенных операций, то следует применять атрибут RequiresRole ("Имя роли"). Так если оператору, как зарегистрированному пользователю, разрешено только просматривать список сотрудников, а операции модификации, удалении и создания новых данных по сотрудника разрешается выполнять пользователю с ролью администратор, соответствующие методы должны быть отмечены атрибутами RequiresRole (" администратор ").

    [RequiresRole("администратор")]
    public void InsertEmployee(Employee employee)
        { . . . }
    
    [RequiresRole("администратор")]
    public void UpdateEmployee(Employee currentEmployee)
            { . . . }
    
    [RequiresRole("администратор")]
    public void DeleteEmployee(Employee employee)
            { . . . }

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

    Ключевые термины

    Службы RIA Services, шаблон Silverlight Business Application, авторизация, аутентификация, регистрация пользователя, локализация приложения, EDM-модель, группировка данных, фильтрация данных, дочернее окно Silverlight, валидация, роль пользователя

    Windows Communication Foundation, WCF, Rich Internet application, RIA, RIA Services, Silverlight Business Application, Domain Service Class, ObjectContext, BusyIndicator, PageSize, AutoCompleteBox, ChildWindow, Authentication, webContext.

    Краткие итоги

    Рассмотрены вопросы построения приложения Silverlight на базе шаблона Business Application. При проектировании бизнес-приложения проанализированы подходы к локализации, созданию EDM-модели данных и службы WCF RIA Services, построению пользовательского интерфейса с использованием источников данных и автоматической генерации представления данных, вопросы сортировки и фильтрации информации в пользовательских интерфейсных элементах, реализацию функция модификации, вставки, удаления и валидации данных, аутентификацию и авторизацию пользователей.

    Ресурсы для углубленного изучения

  • Опубликованы новые материалы по Silverlight и RIA // http://blogs.msdn.com/b/rudevnews/archive/2010/10/10/silverlight-ria.aspx
  • WCF RIA Services // http://msdn.microsoft.com/ru-ru/library/ee707344(en-us,VS.91).aspx.
  • Microsoft Silverlight // http://silverlight.net
  • Silverlight Toolkit // http://silverlight.codeplex.com/
  • Вопросы для самопроверки

  • Поясните назначение служб WCF RIA Services.
  • Как организована аутентификация пользователя с бизнес-приложения Silverlight?
  • Как реализуется локализация бизнес-приложения Silverlight?
  • Зачем класс DomainService помечается атрибутом [EnableClientAccess( )]?
  • Для чего используется атрибут Display в файле метаданных?
  • Для чего используется атрибут Include в файле метаданных?
  • Поясните назначение класса BusyIndicator.
  • Поясните назначение класса PageSize.
  • Как реализуется группировка в источнике данных?
  • Как реализуется фильтрация в источнике данных?
  • Поясните функциональные возможности элемента управления AutoCompleteBox.
  • Поясните назначение валидации данных.
  • Как можно реализовать валидацию в бизнес-приложения Silverlight?
  • Какие способы аутентификации предусмотрены в бизнес-приложения Silverlight?
  • Для чего можно использовать роли в бизнес-приложения Silverlight?
  • Заключение

    При написании данного курса ставилась цель представить основные возможности технологии WPF и Silverlight для разработки приложений. Многие вопросы не вошли в данное пособие. Это прежде всего использование средств разработки Expression Design и Expression Blend, работа со звуком и видео, анимация, деловая графика, вопросы интеграции приложений с другими системами, например SharePoint Services.

    Технологии Microsoft постоянно развиваются, так на момент завершения работы над пособием вышли пакет обновлений Visual Studio 2010 SP1, новая версия библиотеки WCF Data Services , которая предназначена для поддержки открытого стандарта обмена данными OData на платформе .NET, библиотека WCF RIA Services V1.0 SP1 для Silverlight 4, которая упрощает традиционный n-уровневый шаблон приложения путем согласования платформ ASP.NET и Silverlight.

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

    Замечания, пожеления и ваше мнение по данному пособию можно направлять по адресу alexdoljenko@mail.ru.

    Вернуться к учебному плану