При загрузке данных из БД возникает исключение InvalidOperationException с сообщением: Элемент коллекции должен быть пустым перед использованием ItemsSource. Знаю, что для заполнения DataGrid можно использовать коллекции Items или ItemsSource, но одновременно их использовать нельзя: если задано значение для свойства ItemsSource и в коде C# добавляется элемент в Items, возникает исключение. |
Разработка бизнес приложения Silverlight
Задание 2. Создать EDM-модель и сервис данных – 1 час
Создание EDM-модели данных производится аналогично рассмотренноту в лабораторной работе 2. При генерации EDM-модели создается класс сущностей, который наследуется от класса ObjectContext. Класс сущностей содержит описание таблиц базы данных как свойств, метод добавления сущностей и навигации. После создания EDM-модели необходимо перекомпилировать проект.
Создайте службы WCF RIA Services. В Web проекте создайте папку DomainModel в неё добавьте экземпляр класса Domain Service Class с именем EmployeeDomainService.cs ( рис. 11.15).
При добавлении сервиса WCF RIA Services необходимо в окне Add New Domain Service Class установить галочки для поля доступности сервиса для клиента ( Enable client access ), полей сущностей Employee и Title, а также разрешить редактирование сущностей – столбец Enable editing ( рис. 11.16).
В результате добавления EDM-модели и сервиса WCF RIA Services в проекте будут сформированы в папке DataAccess файлы EmployeeModel.edmx, EmployeeModel.Designer.cs, а в папке DomainModel – EmployeeDomainService.cs и EmployeeDomainService.metadata.cs ( рис. 11.17).
Созданный серверный класс EmployeeDomainService является, в общем случае, потомком класса DomainService, в нашем случае – это потомок класса LinqToEntitiesDomainService<PersonalEnterpriceEntities>, который обеспечивает доступ к данным на стороне сервера.
Класс EmployeeDomainService помечен атрибутом [ EnableClientAccess() ], который обеспечивает доступ к сервису со стороны клиента. Методы GetEmployees() и GetTitles() класса предназначены для загрузки в приложение данных из базы, а методы Insert(), Update() и Delete() – для манипулирования данными.
Класс EmployeeDomainService.metadata.cs содержим метаданные о сущностях модели данных.
Метаданные используются, в частности, для генерации элементов контроля в клиентской части приложения и валидации данных. Свойства данных задаются с помощью атрибутов пространства имен System.ComponentModel.DataAnnotations. Если свойства не помечать специальными атрибутами, то в элементах контроля им будут присваиваться метки в соответствии с именами свойств, например FirstName, а порядок формирования элементов, например в элементе контроля DataGrid, будет соответствовать порядку данных в таблице и соответственно для сущности данных. Так, если для свойства Picture необходимо задать метку Фото и в списке свойств в таблице поместить её на первое место, то необходимо для атрибута Display определить свойства Name и Order.
[Display(Name = "Фото", Order = 0)] public byte[ ] Picture { get; set; }
Для свойств класса EmployeeMetadata необходимо указать атрибут [ Display ], который будет использованы в интерфейсных элементах. Для атрибута Titles дополнительно укажем атрибут [ Include ], который предписывает сформировать коллекцию объектов Title в соответствии со свойствами навигации. Код модифицированного класса EmployeeMetadata приведен в приложении А.
При построении сервиса WCF RIA Services на клиенте автоматически генерируется файл EnterpriceBusinessApplication.Web.g.cs, который можно найти в клиентском проекте EnterpriceBusinessApplication, если выделить данный проект и нажать кнопку Показать все файлы ( рис. 11.18).
В файле EnterpriceBusinessApplication.Web.g.cs, в частности, имеются классы сущностей, аналогичные классам серверной части приложения и класс EmployeeDomainContext, который является наследником класса DomainContext и обеспечивает доступ к данным и методам сервиса со стороны клиента.
Задание 3. Разработать клиентскую часть приложения – 3 часа
Разработайте страницу для отображения списка элементов, хранящихся в базе данных. Добавьте в клиентскую часть, в папку Views страницу Silverlight EmployeePage. Для данной страницы в конструкторе откройте вкладку Источник данных и настройте представление данных на странице. Для сущности Employee определим представление в форме DataGrid, для поля Picture – Image, для полей FirstData и FastDate – DatePicker, а для остальных полей – TextBox ( рис. 11.19).
Перетащите источник данных для таблицы Employee в поле конструктора окна EmployeePage. Результат генерации DataGrid для сущности Employee приведен на рис. 11.20.
Добавьте на главной странице Home гиперссылку на созданную страницу EmployeePage, указав для контента расширение разметки для ссылки на строку ресурса ApplicationStrings.EmployeePageTitle.
<HyperlinkButton x:Name="Link3" Style="{StaticResource LinkStyle}" NavigateUri="/EmployeePage" TargetName="ContentFrame" Content="{Binding Path=ApplicationStrings.EmployeePageTitle, Source={StaticResource ResourceWrapper}}"/>
При выполнении приложения в списке сотрудников не выводится фотография и должность. Для отображения фотографии необходимо сделать преобразование двоичного файла в объект Image. Такое преобразование выполняется с помощью конвектора.
Для отображения в сетке 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, который представляет типизированный запрос по отношению к концептуальной модели в данном контексте объекта. Класс имеет ряд методов для формирования запросов к модели данных
В методе 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" />
Индикация загрузки данных. При загрузке страницы приложения происходит загрузка данных из базы данных, что требует определенного времени. Во время загрузки данных на экране дисплея появляется пустая страница и не понятно работает или нет приложение. Устранение такой неопределенности можно реализовать с помощью класса 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 ) к статическому ресурсу. При загрузке данных на экране будет отображаться элемент управления ProgressBar с текстом "Загрузка данных . . . ", что устранит для пользователя неопределенность состояния системы в процессе формирования данных в кэше приложения.
Организация перелистывания страниц. При разработке дизайна страницы приложения имеются много вариантов визуализации данных. Например, можно реализовать организацию разбиения и перелистывания страниц с использованием объекта 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" />
Источником данных ( Source ) для объекта DataPager является объект DomainDataSource – employeeDomainDataSource, а свойство PageSize задает количество строк, выводимых на одной странице.
Группировка по определенному полю. Предположим, что требуется провести группировку по должности сотрудников. Для этого необходимо в 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>
Поиск/фильтрация данных. Вначале модифицируем макет страницы 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, а свойство PropertyPath определяет атрибут фильтрации.
В созданном фильтре для фамилии сотрудника использован оперетор StartsWith, который обеспечивает фильтрацию по первым буквам фамилии сотрудника.
При разработки процесса фильтрации по должности рассмотрим, как можно добавлять новые источники данных.
В папку 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.