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

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

Показывать лекцию целиком

Цель

Освоить основные приемы разработки корпоративных Silverlight- приложений, обеспечивающих взаимодействие клиентской части с базой данных.

Введение в технологию Silverlight

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

В отмечается, что Silverlight имеет следующие преимущества:

  • интеграция Silverlight в существующую платформу, за счет чего возможна разработка Silverlight-приложений на управляемом языке программирования (C# или VB.NET) с поддержкой основных компонент .NET Framework;
  • возможность создания корпоративных приложений, имеющих эффективный интерактивный интерфейс близкий по функциональности к Windows-приложениям, и обеспечивающих работу, как в браузере, так и вне браузера;
  • поддержка Silverlight в интегрированных средствах разработки, таких как Visual Studio 2010 и Expression Blend.
  • В технологии Silverlight используются надстройки браузера, которые обеспечивают следующие средства создания интерактивных приложений:

  • двухмерное рисование с помощью фигур и контуров, которыми можно манипулировать на стороне клиента и добавлять интерактивность за счет реагирования на определенные события;
  • широкий набор элементов управления, заимствованный из технологии WPF, с возможностью манипулирования стилями и шаблонами;
  • анимация, основанная на временных интервалах, и позволяющая определять, что должно произойти и как долго это будет длиться;
  • мультимедиа, предоставляющая средства воспроизведения медиафайлов WMA, WMV7-9, MP3 и VC-1;
  • общеязыковая среда выполнения CLR, которая предоставляет полный набор базовых классов .NET;
  • интернет, посредством которого Silverlight-приложения могут обращаться к традиционным веб-службам ASP.NET или Windows Communication Foundation (WCF), что позволяет объединять клиентский код с безопасными серверными процедурами;
  • связывание данных, что предоставляет удобные способы вывода большого количества данных с помощью небольших фрагментов кода.
  • Технология Silverlight базируется на подмножестве модели WPF. Это обстоятельство определяет следующие общие для Silverlight и WPF особенности:

  • для определения пользовательского интерфейса Silverlight используется разметка XAML, аналогичная используемой в WPF с тем же синтаксисом и возможностью связывания данных;
  • использование многих базовых элементов управления, системы стилизации и механизма шаблонов;
  • использование идентичных фигур, контуров и кистей для рисования;
  • декларативная модель анимации, основанная на последовательности раскадровок, работающая также, как и анимационная модель WPF;
  • использование класса MediaElement для воспроизведения аудио- и видеофайлов.
  • Для изучения технологии проектирования Silverlight- приложения будем испльзовать учебный пример с базой данных Person и приложением, которое реализует стандартные фунции:

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

    Проектирование серверной части приложения

    Основой серверной части Silverlight- приложения является веб-сервер. В рамках веб-сервера создаются источники данных и службы, предоставляющие доступ к данным.

    Взаимодействие приложения Silverlight с источником данных базируется на службе данных WCF и модели "сущность-связь" – Entity Data Model (EDM).

    Концептуальная модель является специфическим представлением структуры некоторых данных в виде сущностей и связей. Одним из способов представления концептуальной модели является схема. На рис 7.1 приведена схема концептуальной модели базы данных Person – "Персонал" с двумя типами сущностей ( Employee – Сотрудник и JobTitle –роль/должность) и одной ассоциативной связью 1:* (один ко многим).

    (рис 7.1) Концептуальная модель базы данных

    Таблица Employee содержит данные по сотруднику. Атрибутами таблицы являются:

  • EmployeeID – код сотрудника;
  • JobRoleIDвнешний ключ для связи с таблицей Title;
  • EmployeeSurname – фамилия;
  • EmployeeName – имя;
  • EmployeePatronymic – отчество;
  • EmployeeStatus – статус;
  • Access – уровень доступа;
  • NetName – сетевое имя;
  • FirstDate – дата приема на работу;
  • LastDate – дата увольнения.
  • Таблица JobTitle является справочником должностей, имеющихся на предприятии, и включает следующие атрибуты:

  • ID – код должности;
  • Title – наименование должности.
  • Разработку Silverlight-приложения, взаимодействующего с базой данных, начнем с конструирования EDM-модели данных. Для этого создадим новое веб-приложение ASP.NET с именем ).

    (рис 7.2) Создание новое веб-приложение ASP.NET

    В проекте будем использовать порт 12345. Для этого откроем окно свойств проекта и на вкладке ).

    (рис 7.3) Создание новое веб-приложение ASP.NET

    Добавим в проект новый элемент – модель ADO.NET EDM, присвоив файлу модели имя Employee.edmx.

    При завершении работы мастера создания EDM-модели в проект будет добавлен файл ).

    (рис 7.4) Проект с моделью EDM

    Службы WCF Data Services входят в состав библиотеки .NET Framework и состоят из набора шаблонов и библиотек. Они позволяют создавать службы данных и обращаться к ним в Интернете, интрасети или в приложении. Службы WCF Data Services предоставляют данные как ресурсы, адресуемые с помощью URI. Это позволяет обращаться к данным и изменять их с использованием семантики архитектурного стиля Representational State Transfer (REST), в частности стандартных команд HTTP, таких как GET, PUT, POST и DELETE.

    Службы WCF Data Services взаимодействуют с сущностей модели EDM и предоставляют ресурсы в виде наборов сущностей и взаимосвязей между ними. Они позволяют предоставлять данные, происходящие из различных источников. Предоставлять реляционные данные в виде ресурсов можно, определив концептуальную модель с помощью Entity Data Model (EDM).

    В состав служб WCF Data Services включен набор клиентских библиотек:

  • для типовых клиентских приложений .NET Framework;
  • для приложений на основе Silverlight.
  • Клиентские библиотеки предоставляют возможности объектно-ориентированного программирования для доступа к службам WCF Data Services из таких платформ, как .NET Framework и Silverlight.

    Создание службы данных начнем с добавления в проект нового элемента – службы данных WCF, файл которой назовем ).

    (рис 7.5) Добавление в проект службы данных

    В объявлении сгенерированного класса WcfDataServicePerson необходимо указать обобщенный тип PersonalEntities (EDM-модель) родительского класса DataService, то есть:

    public class WcfDataServicePerson : DataService<PersonalEntities>

    и права при работе с таблицами базы данных:

    config.SetEntitySetAccessRule("Employees", EntitySetRights.All);
    config.SetEntitySetAccessRule("JobTitles", EntitySetRights.All);

    Это обеспечивает авторизованным клиентам доступ к ресурсам указанных наборов сущностей.

    Отредактированный класс службы данных будет иметь следующий вид:

    public class WcfDataServicePerson: DataService<PersonalEntities >
        {
            public static void InitializeService(DataServiceConfiguration config)
            {
                config.SetEntitySetAccessRule("Employees", EntitySetRights.All);
                config.SetEntitySetAccessRule("JobTitles", EntitySetRights.All);
                config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
            }
        }

    Для тестирования запустим службу. Результаты выполнения приведены на рис 7.6.

    (рис 7.6) Тестирование службы данных

    На этом создание службы данных завершено.

    Проектирование клиентского приложения

    Разрабатываемое клиентское приложение должно обеспечивать чтение информации по сотруднику из базы данных, просмотр, редактирование, обновление, добавление и удаление данных. В качестве клиентского приложения будем использовать Silverlight. Silverlight объединяет возможности сервера, Интернета и персонального компьютера; возможности управляемого кода и динамических языков, декларативного и традиционного программирования, а также мощные средства Windows Presentation Foundation (WPF).

    Silverlight позволяет создавать приложения, используя любой поддерживаемый платформой .NET Framework язык (включая Visual Basic, C# и JavaScript). Visual Studio и Expression Blend предоставляют возможности разработки приложений Silverlight. Можно создавать веб-страницы, использующие как элементы HTML, так и элементы WPF с Silverlight. Подобно HTML, XAML позволяет создать пользовательский интерфейс для веб-приложения с помощью декларативного синтаксиса с тем отличием, что XAML предоставляет значительно более мощные элементы.

    Создадим для нашего решения новый проект Silverlight и назовем его SilverlightAppPersonal (рис 7.7).

    (рис 7.7) Создание нового проекта – приложения Silverlight

    В окне мастера создания приложения Silverlight проверьте размещение приложения и версию Silverlight (рис 7.8). В нашем проекте приложение Silverlight должно размещаться на существующем сайте в решении PersonService, а версия Silverlight должна быть – 4.

    (рис 7.8) Окно мастера создания приложения Silverlight

    В обозревателе решений будет добавлен новый проект ).

    (рис 7.9) Дизайнер решения с приложением Silverlight

    Для проекта ).

    (рис 7.10) Добавление ссылки на службу WcfDataServicePerson

    В обозревателе решений будет добавлена ссылка на службу (рис 7.11).

    (рис 7.11) Ссылка на службу в обозревателе решений

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

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

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

    XAML-код главной страницы приведен в приложении А.

    Созданная страница клиентского приложения имеет все необходимые интерфейсные элементы для обеспечения требуемой функциональности. В XAML-коде описания главной страницы были изменены размеры и введена ссылка на сборку System.Windows.Controls:

    xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"

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

    Формирование запросов к службе данных

    Сервисы WCF Data Services позволяют выполнять запросы к службе данных из клиентского приложения на основе библиотеки .NET Framework с использованием сформированных клиентских классов службы данных. Клиентская библиотека преобразует запрос, определенный на клиенте как экземпляр класса DataServiceQuery, в сообщение запроса HTTP GET. Библиотека получает ответное сообщение и преобразует его в экземпляры классов клиентской службы данных. Эти классы отслеживаются экземпляром DataServiceContext, которому принадлежит объект DataServiceQuery.

    Универсальный класс DataServiceQuery представляет запрос, который возвращает коллекцию экземпляров заданного типа сущности. Запрос к службе данных всегда относится к контексту существующей службы данных. Этот контекст поддерживает URI службы и сведения о метаданных, необходимые для создания и выполнения запроса.

    При выполнении запросов можно явно вызывать метод асинхронного вызова BeginExecute с объектом DataServiceQuery<T> или выполнить запрос LINQ к именованному объекту DataServiceQuery<T>, который получен из контекста DataServiceContext.

    Использование метода асинхронного вызова BeginExecute

    Для загрузки в приложение таблицы Employee базы данных Personal добавим в коде класса MainPage ссылки на библиотеку Data.Services.Client и созданный сервис данных Personal

    using System.Data.Services.Client;
    using SilverlightAppPersonal.Personal;

    а также контекст сущностей context сервиса модели данных EmployeeEntities и экземпляр коллекции employees класса DataServiceCollection<T>

    EmployeeEntities context;
    DataServiceCollection<Employee> employees;

    После внесенных изменений код класса MainPage будет следующим:

    using System.Windows.Controls;
    using System.Data.Services.Client;
    using SilverlightAppPersonal.Personal;
    
    namespace SilverlightAppPersonal
    {
    public partial class MainPage : UserControl
        {
            EmployeeEntities context;
            DataServiceCollection<Employee> employees;
            public MainPage()
            {
                InitializeComponent();
            }
        }
    }

    Класс DataServiceCollection<T> представляет коллекцию динамических сущностей, обеспечивающую выдачу уведомлений при добавлении и удалении из неё элементов или при обновлении списка. Службы данных WCF используют класс DataServiceCollection<T> с целью поддержки привязки извлекаемых из базы данных для элементов управления Silverlight.

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

  • создать экземпляр контекста сущностей сервиса модели данных Personal ;
  • создать экземпляр коллекции employees ;
  • определить событие загрузки коллекции employees.
  • Добавим в XAML-код описание события Loaded класса MainPage со ссылкой на обработчик MainPage_Loaded:

    Loaded="MainPage_Loaded"

    В коде класса MainPage добавим обработчик MainPage_Loaded:

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
            {
                context = new EmployeeEntities(new Uri(
    "WfcDataServicePerson.svc", UriKind.Relative));
                employees = new DataServiceCollection<Employee>();
                employees.LoadCompleted += new 
                   EventHandler<LoadCompletedEventArgs>(employees_LoadCompleted);
                ButtonOpen.IsEnabled = true;
            }

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

    ButtonOpen.IsEnabled = true;

    Загрузка данных из базы данных в приложение будет проведена по щелчку кнопки "Загрузить". В XAML-описании добавим для кнопки ButtonOpen обработчик для события Click - Open_Click.

    <Button Content="Загрузить" Height="25" Name="ButtonOpen" Margin="35,0,0,0" 
            Padding="10,5,10,5" Click="Open_Click" />

    В код класса MainPage добавим метод Open_Click обработчика события Click кнопки ButtonOpen.

    private void Open_Click(object sender, RoutedEventArgs e)
      {
        ResetBindingData();
        DataServiceQuery<Employee> queryEmployee = 
                        context.Employees.AddQueryOption("$orderby", "EmployeeSurname");
        try
        {
            queryEmployee.BeginExecute(OnEmployeeQueryComplete, queryEmployee);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    Метод ResetBindingData() предназначен для очистка коллекции данных employees, списков сотрудников listBoxEmployees и должностей comboBoxTitle:

    private void ResetBindingData()
    {
       employees.Clear();
       listBoxEmployees.ItemsSource = null;
       comboBoxTitle.ItemsSource = null;
       LayoutRoot.UpdateLayout();
    }

    Далее формируется запрос queryEmployee на получение данных, при этом задается режим сортировки данных по фамилии сотрудника ( EmployeeSurname ):

    DataServiceQuery<Employee>  queryEmployee = 
                context.Employees.AddQueryOption("$orderby", "EmployeeSurname");

    С помощью метода AddQueryOption в запрос добавляются параметры. Службы данных WCF поддерживают следующие параметры запросов, приведенные в табл. 7.1.

    Параметры запросов протокола Open Data
    Параметр запроса Описание
    $orderby Определяет порядок сортировки по умолчанию для сущностей в возвращенном канале
    $top Указывает количество сущностей, которые необходимо включить в возвращаемый канал
    $skip Указывает количество сущностей, которые необходимо пропустить перед возвратом сущностей в канал
    $filter Определяет выражение, фильтрующее сущности, которые возвращаются в канал на основе определенного критерия. Этот параметр запроса поддерживает набор операторов логического сравнения, арифметических операторов и заранее заданных функций запроса, которые используются для оценки критерия фильтра
    $expand Указываются связанные сущности, возвращаемые запросом. Связанные сущности включаются либо в качестве канала, либо в качестве записи, встроенной в сущность, возвращаемую запросом
    $format Указывает формат возвращаемого канала. По умолчанию для каналов задается формат Atom
    $select Указывает проекцию, определяющую свойства сущности, возвращаемые в проекции. По умолчанию в канале возвращаются все свойства сущности
    $inlinecount Запрашивает включение в поток количества сущностей, возвращаемых в канале

    Запросы службы данных WCF для Silverlight выполняются асинхронно. Это реализуется методом BeginExecute(), который использует в качестве параметров делегат OnEmployeeQueryComplete и запрос queryEmployee.

    queryEmployee.BeginExecute(OnEmployeeQueryComplete, queryEmployee);

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

    private void OnEmployeeQueryComplete(IAsyncResult result)
    {
        Dispatcher.BeginInvoke(() =>
        {
            DataServiceQuery<Employee> queryEmployee = 
                                 result.AsyncState as DataServiceQuery<Employee>;
            try
            {
                employees.LoadAsync(queryEmployee);
            }
            catch (DataServiceQueryException ex)
            {
                MessageBox.Show(string.Format("Ошибка запроса в БД: {0} - {1}",
                ex.Response.StatusCode.ToString(), ex.Response.Error.Message));
            }
        });
    }

    На данном этапе нам осталось добавить в код класса MainPage делегат employees_LoadCompleted для события LoadCompleted коллекции employees, в котором после завершения формирования коллекции employees задается источник данных для списка listBoxEmployees и производится выделение первого элемента списка.

    private void employees_LoadCompleted(object sender, LoadCompletedEventArgs e)
    {
        if (e.Error == null)
        {
            if (employees.Continuation != null)
            {
                employees.LoadNextPartialSetAsync();
            }
            else
            {
                listBoxEmployees.ItemsSource = employees;
                listBoxEmployees.UpdateLayout();
                if (listBoxEmployees.Items.Count > 0)
                    listBoxEmployees.SelectedIndex = 0;
                ButtonOpen.IsEnabled = false;
                
            }
        }
        else
        {
            MessageBox.Show(string.Format("Ошибка формирования коллекции: {0}",
                                                             e.Error.Message));
            ButtonOpen.IsEnabled = true;
        }
    }

    Использование запроса LINQ

    Класс DataServiceQuery реализует интерфейс IQueryable, определяемый языком LINQ. Клиентская библиотека службы WCF Data Services может преобразовывать запросы LINQ к данным набора сущностей в URI, который представляет выражение запроса, вычисляемое для ресурса службы данных.

    Сформируем запрос LINQ для получения приложением данных сущности Employee.

    var allEmpoyees = from emp in context.Employees
                      orderby emp.EmployeeSurname
                      select emp;

    Формирование коллекции employees класса DataServiceCollection<Employee> для приложения Silverlight может осуществляться только асинхронно, что определяет необходимость использования метода LoadAsync, который требует приведения параметра метода к типу DataServiceQuery<T>, в нашем случае к типу DataServiceQuery<Employee>.

    employees.LoadAsync(allEmpoyees as DataServiceQuery<Employee>);

    При асинхронной загрузке коллекции employees необходимо сформировать и обработать события загрузки коллекции типа employees.LoadCompleted для сущности Employee и события titles.LoadCompleted для сущности JobTitle.

    Для случая использования запроса LINQ метод Open_Click() будет представлен следующим кодом.

    private void Open_Click(object sender, RoutedEventArgs e)
    {
        ResetBindingData();
        listBoxEmployees.ItemsSource = null;
        comboBoxTitle.ItemsSource = null;
        LayoutRoot.UpdateLayout();
        try
        {
            var allEmpoyees = from emp in context.Employees
                              orderby emp.EmployeeSurname
                              select emp;
            employees.LoadAsync(allEmpoyees as DataServiceQuery<Employee>);
            var allTitle = from titl in context.JobTitles
                           select titl;
            titles.LoadAsync(allTitle as DataServiceQuery<JobTitle>);
        }
        catch (DataServiceClientException ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    Следует отметить, что по сравнению с загрузкой данных с использованием метода асинхронного вызова BeginExecute, при применение LINQ-запросов отпадает необходимость использования делегатов OnEmployeeQueryComplete и OnTitleQueryComplete.

    Привязка данных

    Для тестирования нашего приложения необходимо сделать привязку списка listBoxEmployees к данным. Для этого используем шаблон DataTemplate.

    <ListBox Grid.Row="1" Name="listBoxEmployees" HorizontalAlignment="Center" 
                                    Margin="5,2,15,9" Padding="3" Width="364" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Path=EmployeeSurname}" />
                    <TextBlock Text="  " />
                    <TextBlock Text="{Binding Path=EmployeeName}" />
                    <TextBlock Text="  " />
                    <TextBlock Text="{Binding Path=EmployeePatronymic}" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    В списке будем отображать фамилию, имя и отчества сотрудника.

    (рис 7.13) Фрагмент главной страницы с заполненным списком

    Для привязки отображения отдельных данные из списка к детальным данным по сотруднику необходимо для контента основной Grid (LayoutRoot) выполнить привязку к текущему выделению списка listBoxEmployees:

    <Grid x:Name="LayoutRoot" Background="White" Margin="10" 
    VerticalAlignment="Top" Height="590" Width="779"  
    DataContext="{Binding ElementName=listBoxEmployees, Path=SelectedItem, Mode=TwoWay}">

    Начнем привязку детальных данных по сотруднику с текстовых полей: LastName, FirstName, SecondtName и NetName. Для элемента управления TextBox LastName привязка присоединяемого свойства Text к текущему выделению списка listBoxEmployees будет иметь следующий вид:

    Text="{Binding Path= EmployeeSurname, Mode=TwoWay}"

    Для объекта привязки Binding задается только привязываемое свойство ( Path=EmployeeSurname ), а элемент привязки был ранее определен в контенте основной Grid (свойство DataContext ). Свойство Mode задается значение TwoWay, что определяет двустороннюю привязку. Измененный XAML-код для интерфейсных TextBox примет следующий вид:

    <TextBox Name="LastName" Height="25" Margin="5,2,2,2" 
                      Text="{Binding Path=EmployeeSurname, Mode=TwoWay}"></TextBox>
    <TextBox Name="FirstName" Height="25" Margin="2" 
                     Text="{Binding Path=EmployeeName, Mode=TwoWay}"></TextBox>
    <TextBox Name="SecondtName" Height="25" Margin="2" 
                    Text="{Binding Path= EmployeePatronymic, Mode=TwoWay}"></TextBox>
    <TextBox Name="NetName" Height="25" Margin="2" 
                                Text="{Binding Path=NetName, Mode=TwoWay}"></TextBox>

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

    (рис 7.14) Фрагмент главной страницы с детальными данными

    В базе данных Person атрибут EmployeeStatus является целым числом, значение которого соответствует индексу списка ComboBox comboBoxStatus приложения. Функциональность приложения определяет следующий список статуса сотрудника: не задано, активен, выходной, в отпуске, болеет, не работает. Привязка для данного элемента управления выполняется для свойства SelectedIndex.

    <ComboBox Name="comboBoxStatus" Height="25" Margin="2" 
    SelectedIndex = "{Binding Path=EmployeeStatus, Mode=TwoWay, BindsDirectlyToSource=True}">
            <ComboBoxItem Content="не задано" />
            <ComboBoxItem Content="активен" />
            <ComboBoxItem Content="выходной" />
            <ComboBoxItem Content="в отпуске" />
            <ComboBoxItem Content="болеет" />
            <ComboBoxItem Content="не работает" />
            <ComboBoxItem Content="помечен как удаленный" />
    </ComboBox>

    Уровень доступа сотрудника к ресурсам информационной системы – Access в базе данных хранится в виде символьной строки. Однако перечень уровней доступа в системе фиксирован и меняется редко. Для обеспечения работы с ограниченным списком уровней доступа пользователей введем в приложение специальный класс Access. Для этого добавим в приложение SilverlightAppPersonal класс Access, имеющий свойство AccessRole, характеризующее уровень доступа сотрудника к ресурсам сети.

    public class Access
        {
            public string AccessRole { set; get; }
            public Access(string accessRole)
            {
                AccessRole = accessRole;
            }
        }
    
    В коде класса MainPage добавим ссылку на библиотеку 
    System.Collections.ObjectModel.
    
    using System.Collections.ObjectModel;
    
    Затем определим коллекцию уровней доступа accesses.
    
    ObservableCollection<Access> accesses;

    В коде класса MainPage добавим ссылку на библиотеку System.Collections.ObjectModel.

    using System.Collections.ObjectModel;

    Затем определим коллекцию уровней доступа accesses.

    ObservableCollection<Access> accesses;

    В конструкторе класса MainPage создадим экземпляр коллекции accesses и сформируем её из следующего списка: не задано, оператор, старший оператор, начальник смены, администратор, аналитик. Измененный код конструктора класса MainPage приведен ниже.

    public MainPage()
      {
          InitializeComponent();
           accesses = new ObservableCollection<Access>();
                accesses.Add(new Access("не задано"));
                accesses.Add(new Access("оператор"));
                accesses.Add(new Access("старший оператор"));
                accesses.Add(new Access("начальник смены"));
                accesses.Add(new Access("администратор"));
                accesses.Add(new Access("аналитик"));
                comboBoxAccess.ItemsSource = accesses;
      }

    После формирования коллекции её значение присваивается источнику данных выпадающего списка comboBoxAccess.

    comboBoxAccess.ItemsSource = accesses;

    В XAML-коде класса MainPage для выпадающего списка comboBoxAccess добавим привязку для свойства SelectedValue и зададим значение SelectedValuePath для вывода в ComboBox.

    <ComboBox Height="25" Name="comboBoxAccess" Margin="5,2,2,2" 
    SelectedValue="{Binding Path=Access, Mode=TwoWay}"
     SelectedValuePath="AccessRole" DisplayMemberPath="AccessRole " />

    Должности сотрудников хранятся с отдельной таблице JobTitle базы данных Person. Перед загрузкой в приложение таблицы JobTitle немного усовершенствуем созданный ранее код. В методе Open_Click() всё, что связано с загрузкой в приложение данных таблицы Employee выделим в отдельный метод LoadEmployees():

    private void LoadEmployees()
    {
        DataServiceQuery<Employee> queryEmployee = 
                           context.Employees.AddQueryOption("$orderby", "EmployeeSurname");
        try
        {
            queryEmployee.BeginExecute(OnEmployeeQueryComplete, queryEmployee);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    С учетом введения метода LoadEmployees() метод Open_Click() примет следующий вид:

    private void Open_Click(object sender, RoutedEventArgs e)
    {
        LoadEmployees();
    }

    Загрузка данных таблицы JobTitle выполняется аналогично загрузке данных таблицы Employee.

    Определим в классе MainPage коллекцию titles.

    DataServiceCollection<JobTitle> titles;

    В методе MainPage_Loaded добавим код создания экземпляра titles коллекции

    DataServiceCollection< JobTitle >:
    titles = new DataServiceCollection<JobTitle>();

    и зарегистрируем для коллекции событие LoadCompleted с делегатом titles_LoadCompleted:

    titles.LoadCompleted +=new EventHandler<LoadCompletedEventArgs>(titles_LoadCompleted);

    Измененный метод MainPage_Loaded будет иметь следующий вид:

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        context = 
               new PersonalEntities(new Uri("WfcDataServicePerson.svc", UriKind.Relative));
        employees = new DataServiceCollection<Employee>();
        titles = new DataServiceCollection<JobTitle>();
        employees.LoadCompleted += 
                     new EventHandler<LoadCompletedEventArgs>(employees_LoadCompleted);
        titles.LoadCompleted += 
                                 new EventHandler<LoadCompletedEventArgs>(titles_LoadCompleted);
        ButtonOpen.IsEnabled = true;
    }

    По аналогии с методом LoadEmployees() создадим метод LoadTitles().

    private void LoadTitles()
    {
        DataServiceQuery<JobTitle> queryTitle = context.JobTitles;
        try
        {
            queryTitle.BeginExecute(OnTitleQueryComplete, queryTitle);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    Далее спроектируем код делегата асинхронного запроса к службе данных.

    private void OnTitleQueryComplete(IAsyncResult result)
    {
        Dispatcher.BeginInvoke(() =>
        {
            DataServiceQuery<JobTitle> queryTitle = result.AsyncState as DataServiceQuery<JobTitle>;
            try
            {
                titles.LoadAsync(queryTitle); 
            }
            catch (DataServiceQueryException ex)
            {
                MessageBox.Show(string.Format("Ошибка запроса в БД: {0} - {1}",
                ex.Response.StatusCode.ToString(), ex.Response.Error.Message));
            }
        }
        );
    }

    Делегат обработки события формирования коллекции типа DataServiceCollection<JobTitle> будет иметь следующий вид:

    private void titles_LoadCompleted(object sender, LoadCompletedEventArgs e)
    {
        if (e.Error == null)
        {
            if (titles.Continuation != null)
            {
                titles.LoadNextPartialSetAsync();
            }
            else
            {
                comboBoxTitle.ItemsSource = titles;
                comboBoxTitle.UpdateLayout();
            }
        }
        else
        {
            MessageBox.Show(string.Format("An error has occured: {0}", e.Error.Message));
        }
    }

    Добавим в код метода Open_Click() вызов метода LoadTitles().

    private void Open_Click(object sender, RoutedEventArgs e)
    {
        LoadEmployees();
        LoadTitles();
    }

    В XAML-коде класса MainPage для выпадающего списка comboBoxTitle добавим привязку для свойства SelectedValue, SelectedValuePath и зададим значение DisplayMemberPath для вывода в ComboBox, а также обработчик события SelectionChanged:

    <ComboBox Height="25" Name="comboBoxTitle" Margin="5,2,2,2" 
    SelectedValue="{Binding Path=JobRoleID,  Mode=TwoWay}" 
    SelectedValuePath="ID" DisplayMemberPath="Title" />

    Следует пояснить, что привязка SelectedValue="{Binding Path=JobRoleID, Mode=TwoWay}" и задание свойства для привязки SelectedValuePath="ID" используют поля JobRoleID и ID таблицы Employee через контент Grid, а выводимое в ComboBox значение задается как DisplayMemberPath="Title" и при этом используется атрибут Title таблицы JobTitle коллекции titles, являющейся источником данных для comboBoxTitle.

    Для календаря DatePicker необходимо построить привязку для свойства SelectedDate.

    <controls:DatePicker Name="datePickerFirstDate" 
                SelectedDate="{Binding Path=FirstDate, Mode=TwoWay}" Margin="5,2,2,2"/>
    <controls:DatePicker Name="datePickerLastDate" 
                SelectedDate="{Binding Path=LastDate, Mode=TwoWay}"  Margin="5,2,2,2" />

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

    (рис 7.15) Фрагмент главной страницы с привязанными данными

    Управление доступностью кнопок приложения

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

    На странице приложения размещены шесть кнопок: Сохранить, Отменить, Создать, Загрузить, Редактировать и Удалить. Кнопки могут быть доступны или недоступны пользователю при работе приложения. Это задается свойством IsEnable. Доступность кнопок определяется состоянием, в котором находится приложение. В тоже время нажатие какой-либо кнопки переводит, как правило, приложение в какое-либо другое состояние. Для проектируемого приложения можно определить следующие состояния:

  • первоначальная загрузка страницы (1);
  • просмотр данных по всем сотрудникам (2);
  • редактирование данных по отдельному сотруднику (3);
  • создание новой записи по сотруднику в базе данных (4).
  • На рис 7.16 приведена диаграмма состояний приложения, на которой кружками обозначены состояния, а дуги соответствуют переходам при нажатии определенной кнопки.

    (рис 7.16) Диаграмма состояний приложения

    На основе диаграммы состояний построим таблицу доступности кнопок в различных состояниях (таблица 7.2).

    Доступность кнопок в различных состояниях приложения
    Состояние Доступность кнопок - IsEnable
    Сохранить Отменить Создать Загрузить Редактировать Удалить
    1 false false false true false false
    2 false false true false true true
    3 true true false false false false
    4 true true false false false false

    В XAML-коде класса MainPage все кнопки определены как недоступные, то есть свойство IsEnable –false. После загрузки страницы (состояние 1) кнопка Загрузить становится доступной, а после загрузки данных из базы во всех остальных состояниях она недоступна. Режим доступности кнопок Сохранить, Отменить, Создать, Редактировать и Удалить в состояниях 2,3 и 4 изменяется в соответствии данными, приведенными в табл. 6.2. Для управления доступностью кнопок добавим в код класса MainPage метод ButtonIsEnabled().

    private void ButtonIsEnabled(bool enabele)
    {
        ButtonEdit.IsEnabled = enabele;
        ButtonSave.IsEnabled = !enabele;
        ButtonNew.IsEnabled = enabele;
        ButtonUndo.IsEnabled = !enabele;
        ButtonDelete.IsEnabled = enabele;
    }

    Кроме управления доступностью кнопок в приложении целесообразно управлять и возможностью редактирования детальных данных по сотруднику. Детальные данные должны быть доступны для редактирования в состояниях редактирования (3) и создания (4) новой записи, а в режиме просмотра – должны быть доступны только для чтения. Для управления доступностью детальных данных по сотруднику добавим в код класса MainPage метод ReadOnly().

    private void ReadOnly(bool readOnly)
    {
        LastName1.IsReadOnly = !readOnly;
        FirstName1.IsReadOnly = !readOnly;
        SecondtName1.IsReadOnly = !readOnly;
        NetName1.IsReadOnly = !readOnly;
        comboBoxTitle.IsEnabled = readOnly;
        comboBoxAccess.IsEnabled = readOnly;
        comboBoxStatus.IsEnabled = readOnly;
        datePickerFirstDate.IsEnabled = readOnly;
        datePickerLastDate.IsEnabled = readOnly;
    }

    В XAML-коде класса MainPage для кнопок Сохранить, Отменить, Создать, Редактировать и Удалить добавим событие Click и обработчики событий Save_Click(), Undo_Click(), New_Click(), Edit_Click() и Delete_Click().

    <Button Name="ButtonSave" Content="Сохранить" Margin="5" Padding="5"
     Height="25" IsEnabled="False" Click="Save_Click" />
    <Button Name="ButtonUndo" Content="Отменить" Margin="5" Padding="5" 
    Height="25" IsEnabled="False" Click="Undo_Click"/>
    <Button Name="ButtonNew" Content="Создать" Margin="5" Padding="5" 
    Height="25" IsEnabled="False" Click="New_Click"/>
    <Button Name="ButtonEdit"  Content="Редактировать" Margin="5" 
    Padding="5" Height="25" IsEnabled="False" Click="Edit_Click"/>
    <Button Name="ButtonDelete"  Content="Удалить" Padding="5" 
    Margin="35,0,0,0" Height="25" IsEnabled="False" Click="Delete_Click" />

    При нажатии кнопки Удалить не изменяется состояние приложения – это видно из рис 7.16, где действию Удалить соответствует петля для состояния 2, поэтому на текущем этапе разработки приложения не будем определять код метода и Delete_Click(), а для остальных созданных методов добавим код для управления доступности кнопок и элементов управления.

    private void Save_Click(object sender, RoutedEventArgs e)
    {
        ReadOnly(false);
        ButtonIsEnabled(true);
        listBoxEmployees.IsEnabled = true;
    }
    
    private void Undo_Click(object sender, RoutedEventArgs e)
    {
        ReadOnly(false);
        ButtonIsEnabled(true);
        listBoxEmployees.IsEnabled = true;
    }
    
    private void Edit_Click(object sender, RoutedEventArgs e)
    {
        ButtonIsEnabled(false);
        listBoxEmployees.IsEnabled = false;
        ReadOnly(true);
    }
    
    private void New_Click(object sender, RoutedEventArgs e)
    {
        ButtonIsEnabled(false);
        ReadOnly(true);
        listBoxEmployees.IsEnabled = false;
    }
    
    private void Delete_Click(object sender, RoutedEventArgs e)
    {
        MessageBoxResult result = MessageBox.Show("Удалить сотрудника: " + 
          emp.EmployeeSurname + " " + emp.EmployeeName + " " + emp.EmployeePatronymic,
         "Предупреждение", MessageBoxButton.OKCancel);
        if (result == MessageBoxResult.OK)
        {
            // Удалить выбранного сотрудника из коллекции данных
        }
    }

    Кроме обработчиков событий кнопок необходимо внести изменения в код метода employees_LoadCompleted(), который завершает загрузку данных из базы по нажатию кнопки Загрузить.

    private void employees_LoadCompleted(object sender, LoadCompletedEventArgs e)
    {
        if (e.Error == null)
        {
            if (employees.Continuation != null)
            {
                employees.LoadNextPartialSetAsync();
            }
            else
            {
                listBoxEmployees.ItemsSource = employees;
                listBoxEmployees.UpdateLayout();
                if (listBoxEmployees.Items.Count > 0)
                    listBoxEmployees.SelectedIndex = 0; 
    
                ButtonIsEnabled(true);
                ReadOnly(false);
                ButtonOpen.IsEnabled = false;
            }
        }
        else
        {
            MessageBox.Show(string.Format("An error has occured: {0}", e.Error.Message));
            ButtonOpen.IsEnabled = true;
        }
    }

    Редактирование данных

    Для редактирования данных по сотруднику необходимо обеспечить возможность изменения детальных данных, что реализуется нажатием кнопки Редактировать, которая задает свойству IsReadOnly элементов управления TextBox и свойству IsEnabled элементов управления ComboBox и DatePicker значение true. С учетом того, что для элементов управления детальными данными выполнена двусторонняя привязка данных ( Mode=TwoWay ), то изменения в детальных данных будут приводить к изменению данных в контексте сущностей PersonalEntities, связанных со списком listBoxEmployees. После редактирования данных по сотруднику необходимо проведенные изменения сохранить в базе данных. Для этого модифицируем метод Save_Click, добавив в него следующую строку:

    context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context);

    Измененный код метода Save_Click будет иметь следующий вид:

    private void Save_Click(object sender, RoutedEventArgs e)
    {
        context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context);
        ReadOnly(false);
        ButtonIsEnabled(true);
        listBoxEmployees.IsEnabled = true;
    }

    Метод BeginSaveChanges() класса DataServiceContent запускает асинхронную операцию сохранения данных, изменение которых были проведены в контексте сущностей PersonalEntities. Перечисление SaveChangesOptions.Batch определяет, что все проведенные изменения сохраняются в составе одного пакетного запроса. Вторым параметром метода BeginSaveChanges() является делегат, который ссылается на метод OnChangesSaved(). Метод OnChangesSaved() обрабатывает результат этой асинхронной операции путем вызова метода EndSaveChanges(), анализа возвращаемых объектов OperationResponse и печати кодов состояния ответа.

    private void OnChangesSaved(IAsyncResult result)
    {
        Dispatcher.BeginInvoke(() =>
        {
            context = result.AsyncState as PersonalEntities;
            try
            {
                WriteOperationResponse(context.EndSaveChanges(result));
            }
            catch (DataServiceRequestException ex)
            {
                       MessageBox.Show( WriteOperationResponse(ex.Response));
            }
            catch (InvalidOperationException ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
        );
    }

    Метод WriteOperationResponse() обеспечивает формирование информации при сохранении данных.

    private string WriteOperationResponse(DataServiceResponse response)
    {
        string mes = string.Empty;
        int i = 1;
        if (response.IsBatchResponse)
        {
            mes = string.Format("Коды серии операций сохранения: {0}\n",
                response.BatchStatusCode);
        }
        foreach (ChangeOperationResponse change in response)
        {
            mes +=
                string.Format("\tИзменение {0} code: {1}\n",
                i.ToString(), change.StatusCode.ToString());
            if (change.Error != null)
            {
                string.Format("\tИзменение {0} error: {1}\n",
                    i.ToString(), change.Error.Message);
            }
            i++;
        }
        return mes;
    }

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

    Создание новых данных по сотруднику

    Для создания новых записей по сотрудникам в базе данных изменим код метода New_Click().

    private void New_Click(object sender, RoutedEventArgs e)
    {
        Employee newEmployee = 
                       Employee.CreateEmployee(0, "не задано", "не задано", 0, "не задано");
        newEmployee.Access = "не задано";
        newEmployee.EmployeeStatus = 0;
        newEmployee.NetName = "guest";
        newEmployee.FirstDate = DateTime.Now;
        try
        {
            context.AddToEmployees(newEmployee);
            context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context);
            employees.Add(newEmployee);
            int count = listBoxEmployees.Items.Count;
            listBoxEmployees.SelectedIndex = count - 1;
            ButtonIsEnabled(false);
            ReadOnly(true);
            listBoxEmployees.IsEnabled = false;
        }
        catch (DataServiceRequestException ex)
        {
            MessageBox.Show("Ошибка добавления нового сотрудника в базу данных");
        }
    }

    Вначале для класса Employee модели сущностей Personal создаем новый экземпляр newEmployee с помощью метода CreateEmployee():

    Employee newEmployee=Employee.CreateEmployee(0,
                "не задано","не задано",0,"не задано");

    Метод CreateEmployee() автоматически сгенерирован при создании EDM-модели сущностей и в качестве параметров использует только обязательные поля таблицы Employee базы данных Personal. Следующие строки кода предназначены для задания необязательных полей таблицы Employee ( Access, EmployeeStatus, NetName и FirstDate ):

    newEmployee.Access = "не задано";
    newEmployee.EmployeeStatus = 0;
    newEmployee.NetName = "guest";
    newEmployee.FirstDate = DateTime.Now;

    Добавление новой сущности newEmployee в модель Personal производится следующей строкой кода:

    context.AddToEmployees(newEmployee);

    при этом используется метод AddToEmployees().

    Затем посредством метода BeginSaveChanges() класса DataServiceContent запускаем сохранение данных в контексте сущностей PersonalEntities.

    context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context);

    Для продолжения работы приложения необходимо внести изменения в коллекцию employees.

    employees.Add(newEmployee);

    и установить фокус в списке listBoxEmployees на созданной записи:

    int count = listBoxEmployees.Items.Count;
    listBoxEmployees.SelectedIndex = count - 1;

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

    Для удаления данных по сотруднику в базе данных необходимо определить экземпляр удаляемого объекта из списка listBoxEmployees.

    Employee emp = (Employee)listBoxEmployees.SelectedItem;

    Затем удалим экземпляр класса Employee из контекста сущностей PersonalEntities с помощью метода DeleteObject().

    context.DeleteObject(emp);

    После изменения индекса выделения в списке listBoxEmployees необходими удалить экземпляр emp класса Employee из коллекции employees.

    listBoxEmployees.SelectedIndex =
           listBoxEmployees.SelectedIndex == 0 ? 1 : listBoxEmployees.SelectedIndex - 1;
    employees.Remove(emp);

    Посредством метода BeginSaveChanges() класса DataServiceContent запускаем сохранение изменений при удалении данных из контекста сущностей PersonalEntities.

    context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context);

    Окончательный код метода Delete_Click() будет иметь следующий вид.

    private void Delete_Click(object sender, RoutedEventArgs e)
    {
        Employee emp = (Employee)listBoxEmployees.SelectedItem;
        MessageBoxResult result = MessageBox.Show("Удалить сотрудника: " + emp.EmployeeSurname + " " 
        + emp.EmployeeName + " " + emp.EmployeePatronymic,
                       "Предупреждение", MessageBoxButton.OKCancel);
        if (result == MessageBoxResult.OK)
        {
            context.DeleteObject(emp);
            listBoxEmployees.SelectedIndex =
            listBoxEmployees.SelectedIndex == 0 ? 1 : listBoxEmployees.SelectedIndex - 1;
            employees.Remove(emp);
            context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context);
        }
    }

    Процесс разработки Silverlight-приложения, проведенный в лекциях 5 и 6 настоящего пособия, отражает основные этапы создания корпоративных веб-приложений, имеющих клиент-серверную архитектуру.

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

    Технология Silverlight , браузер, веб-сервер, модель "сущность-связь", модель ADO.NET, службы WCF Data Services, язык LINQ, привязка данных, шаблон DataTemplate, редактирование данных, создание новых данных, удаление данных.

    Silverlight, Entity Data Model, EDM, .NET Framework, DataService, UserControl, DataServiceContext, DataServiceQuery, BeginExecute, Binding, Path, BeginSaveChanges, SaveChangesOptions, OnChangesSaved, Dispatcher, BeginInvoke, ChangeOperationResponse, DeleteObject.

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

    Проиллюстрированы возможности построения корпоративного Silverlight-приложения, включающего серверную часть на базе веб-приложение ASP.NET, источник данных в виде базы данных для СУБД SQL Server, службу данных, клиентскую часть системы, позволяющую просматривать, редактировать, удалять и создать данные по сотрудникам предприятия.

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

  • Silverlight // http://msdn.microsoft.com/ru-ru/library/cc838158(v=VS.95).aspx.
  • [], стр. 5 – 134.
  • [], стр. 17 - 238
  • [], стр. 17 – 85.
  • [], стр.1005 – 1024, 1234 – 1256.
  • [], стр. 906 - 960
  • Вопросы для самопроверки

  • Поясните назначение технологии Silverlight.
  • Какие преимущества имеет технология Silverlight?
  • Какие особенности имеет технология Silverlight?
  • Поясните назначение службы WCF Data Services.
  • Какой набор клиентских библиотек включен в состав служб WCF Data Services?
  • Каким способом для клиентского приложения Silverlight обеспечивается доступ к серверным службам WCF Data Services?
  • Как можно выполнить запрос к службе данных из клиентского приложения?
  • Поясните назначение класса DataServiceContext.
  • Поясните назначение класса DataServiceQuery<T>.
  • Почему необходимо использовать метод BeginExecute() для выполнения запроса службы данных WCF ?
  • Поясните назначение метода LoadAsync.
  • Как осуществляется привязка интерфейсных элементов к источнику данных службы WCF Data Services?
  • Как осуществляется привязка данных к элементу контроля ComboBox?
  • Как осуществляется привязка данных для связанных таблиц источника данных?
  • Как можно управлять доступностью элементов контроля в Silverlight приложении?
  • Поясните назначение метода BeginSaveChanges().
  • Поясните назначение метода OnChangesSaved().
  • С помощью какого метода можно создать новую запись в источнике данных службы WCF Data Services?
  • Какой метод службы WCF Data Services используется для удаления данных источнике данных?
  • Вернуться к учебному плану