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

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

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

Цель

В процессе выполнения работы необходимо разработать браузерное/внебраузерное Silverlight-приложения

Задание 1. Разработать серверные компоненты приложения – 2 час.

Создайте новое веб-приложение ASP.NET, например с именем PersonService (рис 9.1). Имя приложения целесообразно связать с вариантом разрабатываемтй информационной подсистемы

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

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

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

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

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

).

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

В объявлении сгенерированного класса 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;
        }
    }

Результаты запуска службы данных приведены на рис 9.4.

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

Задание 2. Разработать интерфейс клиентской части приложения – 2 часа.

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

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

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

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

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

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

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

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

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

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

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

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

Задание 3. Разработать запросы к службе данных– 2 часа.

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

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

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

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

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

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

Для загрузки в приложение таблицы 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;
            }

    Загрузка данных из базы данных в приложение будет проведена по щелчку кнопки "Загрузить". В 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 для 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

    Подход к выполнению запрос LINQрассмотрим не примере базы данных Personal. В лабораторной работе необходимо использовать базу данных в соответствии с вариантом задания.

    Класс 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.

    Задание 4. Произвести привязку данных к элементам контроля – 2 часа

    Привязка данных к элементу списка ListBox. При использовании в приложении списка ListBox необходимо выполнить привязку к нему данных. Методика привязки данных к списку ListBox рассматривается на примере списка сотрудников базы данных Personal.

    Для привязки используем шаблон 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>

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

    Для привязки отображения отдельных данные из списка к детальным данным по сотруднику необходимо для контента основной 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}">

    Для элемента управления TextBox привязка присоединяемого свойства Text к текущему выделению списка listBox будет иметь следующий вид:

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

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

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

    Для обеспечения работы с ограниченным списком добавьте в приложение специальный класс 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 создайте экземпляр коллекции 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;
      }

    После формирования коллекции 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 " />

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

    Должности сотрудников хранятся с отдельной таблице 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" />

    Задание 5. Разработать методы манипулирования данными – 2 часа.

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

    Для редактирования данных необходимо обеспечить возможность изменения детальных данных, что реализуется нажатием кнопки Редактировать. С учетом того, что для элементов управления детальными данными выполнена двусторонняя привязка данных ( Mode=TwoWay ), то изменения в детальных данных будут приводить к изменению данных в контексте сущностей, связанных со списком listBox. После редактирования данных необходимо проведенные изменения сохранить в базе данных. Для этого модифицируйте метод 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;
    }

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

    Создание новых данных

    Создание новых записей в базе данных рассматривается на примере базы Personal.

    Для создания новых записей в базе данных изменим код метода 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;

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

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

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

    Задание 6. С помощью стилей и шаблонов модифицировать представление интерфейсных элементов – 2 часа.

    Конвертор графических данных

    Для реализации привязки графических данных модели с элементом управления Image необходимо создание класса преобразователя значений. Класс конвертора должен реализовывать интерфейс IValueConverter и содержать два метода Convert() и ConvertBack() для прямого и обратного преобразования данных. Код класса ImageConverter конвертора двоичных данных в графический формат имеет следующий вид.

    public class ImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
                           object parameter, CultureInfo culture)
        {
            BitmapImage bi = new BitmapImage();
            if (value != null)
            {
                if (value != null)
                    bi.SetSource(new MemoryStream((Byte[])value));
                return bi;
            }
            return bi;
        }
        public object ConvertBack(object value, Type targetType, 
                               object parameter, CultureInfo culture)
        {
            throw new NotImplementedException("Отсутствует ConvertBack");
        }
    }

    Метод Convert() осуществляет прямое преобразование массива байт Byte[] в объект класса BitmapImage, то есть изменяет источник данных перед их передачей целевому объекту для отображения в пользовательском интерфейсе. Параметрами метода является:

  • value типа object – исходные данные, передаваемые целевому объекту, в нашем случае массива байт;
  • targetType – тип данных, ожидаемый целевым свойством зависимостей;
  • parameter – необязательный параметр для использования в логике преобразователя;
  • culture – язык и региональные параметры преобразования.
  • Возвращаемое значение метода Convert() – object, которое в нашем случае является объектом bi класса BitmapImage.

    В методе Convert() вначале создается экземпляр bi класса BitmapImage.

    BitmapImage bi = new BitmapImage();

    Если входное значение параметра value определено, то задается источник для объекта bi класса BitmapImage на основании потока MemoryStream из массива байт.

    if (value != null)
    {
        if (value != null)
            bi.SetSource(new MemoryStream((Byte[])value));
        return bi;
    }

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

    Модификация шаблона данных списка ListBox

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

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
    . 
    </Grid>

    В первую колонку поместим интерфейсный элемент Image для размещения фотографии.

    <Image Grid.Column="0"
                  Source="{Binding Path=Picture, Mode=OneWay, 
                  Converter={StaticResource ImageConverter}}" 
                  Height="87" Width="60" Margin="5" 
                  Name="Image" 
                  Stretch="Uniform"   />

    В качестве источника для интерфейсного элемента Image зададим привязку к полю Picture объекта Employee, режим односторонней привязки ( Mode=OneWay ), а также конвертор, ссылающийся на статический ресурс. Предварительно необходимо определить пространство имен, где расположен класс конвертора

    xmlns:converter="clr-namespace:DataBindingPersonal.Converters"

    и затем определить ресурс для основного окна приложения

    <UserControl.Resources>
        <converter:ImageConverter x:Key="ImageConverter" />
    </UserControl.Resources>

    Во вторую колонку сетки поместим текстовые поля с данными по сотруднику.

    <StackPanel  Grid.Column="1" Orientation="Vertical">
    	<TextBlock Text="{Binding Path=EmployeeSurname}" Margin="5,5,5,5" />
    	<TextBlock Text="{Binding Path=EmployeeName}" Margin="5" />
    	<TextBlock Text="{Binding Path=EmployeePatronymic}" Margin="5" />
    </StackPanel>

    Модифицированная XAML-разметка для интерфейсного элемента listBoxEmployees примет следующий вид.

    <ListBox Grid.Row="1" Name="listBoxEmployees" HorizontalAlignment="Center" 
             Margin="11,2,29,9" Padding="3" Width="300" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Image Grid.Column="0" Source="{Binding Path=Picture, Mode=OneWay, 
                        Converter={StaticResource ImageConverter}}" 
                           Height="87" Width="60" Margin="5" 
                           Name="Image" Stretch="Uniform"   />
                    <StackPanel  Grid.Column="1" Orientation="Vertical">
                    <TextBlock Text="{Binding Path=EmployeeSurname}" Margin="5,5,5,5" />
                    <TextBlock Text="{Binding Path=EmployeeName}" Margin="5" />
                    <TextBlock Text="{Binding Path=EmployeePatronymic}" Margin="5" />
                </StackPanel>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    Изменение визуального поведения элемента ListBox

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

    Для примера поставим задачу создания следующего визуального представления и поведения элемента управления ListBox – listBoxEmployees:

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

    Для реализации требуемого визуального представления и поведения элемента управления ListBox спроектируем три стиля:

  • стиль, используемый при визуализации контейнеров элементов ListBox – ListBoxItemStyle ;
  • стиль, используемый для элемента контроля ListBox - ListBoxPhotoStyle;
  • стиль, используемый для прокручиваемой области, в которой могут содержаться другие видимые элементы – ScrollViewerPhotoStyle.
  • Кроме того, необходимо выделить в отдельный ресурс шаблон используемый для формирования данных отдельного элемента списка ListBox – ListBoxItemPhotoDataTemplate.

    Стиль ListBoxItemStyle создается для типа ListBoxItem (TargetType="ListBoxItem") и в нем определяются четыре свойства: Padding, VerticalAlignment, HorizontalAlignment и Template.

    Style x:Key="ListBoxItemStyle" TargetType="ListBoxItem">
        <Setter Property="Padding" Value="1"/>
        <Setter Property="VerticalAlignment" Value="Top"/>
        <Setter Property="HorizontalAlignment" Value="Left"/>
        <Setter Property="Template">
            <Setter.Value>
    ...
                     <!—Определение вложенного свойства ControlTemplate -->
    ...
            </Setter.Value>
        </Setter>
    </Style>

    Основная задача при формировании визуального представления и поведения элемента управления ListBoxItem сводится к заданию вложенного свойства ControlTemplate. Это определяется тем, что визуальная структура и визуальное поведение интерфейсного элемента задается в его шаблоне ControlTemplate.

    <ControlTemplate TargetType="ListBoxItem">
      <Border
           Name="Border"
    	Padding="5"
    	Width="200" Height="126"
    	CornerRadius="5"
           Background="{StaticResource ListBoxItemPhotoActiveBGSolidBrush}">
                <!—Определение визуального поведения в различных состояниях -->. . .
         <Border 
    BorderBrush="{StaticResource PhotoSelectedBGSolidBrush}"
    	BorderThickness="1"
    	CornerRadius="5">
           <ContentPresenter x:Name="contentPresenter" />
         </Border>
      </Border>
    </ControlTemplate>

    Шаблон ControlTemplate содержит две рамки. Внешняя рамка имеет имя Border, а внутренняя - задана без имени. Свойства заливки фона Background, для внешней рамки, и контура BorderBrush, для внутренней рамки, заданы в расширенной разметке ссылкой на статические ресурсы.

    Свойство ContentPresenter внутренней рамки интерфейсного элемента ListBoxItem определяет где должно отображаться содержание ( Content ) данного элемента.

    Визуальное поведение задает способ отображения элемента управления в определенных состояниях. Для управления состояниями и логикой переходов между состояниями элементов управления используется класс VisualStateManager, который в XAML-разметке стиля представлен одноименным свойством. Данное свойство позволяет указывать состояния для элемента управления, его внешний вид в определенном состоянии, и порядок изменения состояния элемента управления. Внешний вид элемента управления, находящегося в некотором состоянии, определяется классом VisualState. Данный класс содержит коллекцию объектов Storyboard, указывающих, как изменяется внешний вид элемента управления в определенном состоянии. Состояния просмотра добавляются в элемент управления посредством вложенного свойства зависимостей VisualStateManager.VisualStateGroups элемента управления. Для проектируемого стиля необходимо задать визуальное представление для состояний CommonStates и SelectionStates.

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
    ...
                <!—Описание состояний и поведения в этих состояниях -->...
    ...
        </VisualStateGroup>
        <VisualStateGroup x:Name="SelectionStates">
    ...
                <!—Описание состояний и поведения в этих состояниях -->...
    ...
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    Каждый объект VisualStateGroup содержит коллекцию взаимоисключающих объектов VisualState. Элемент управления всегда находится в точности одном состоянии в каждой группе VisualStateGroup.

    Для группы CommonStates определим функциональность для состояний Normal и MouseOver. При наведении указателя мыши на элемент списка ListBoxItem (состояние MouseOver ) свойство ColorAnimation определяет процесс анимации значения свойства Background (Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)") для объекта Border (Storyboard.TargetName="Border") от исходного значения к целевому (To="{StaticResource PhotoSelectedBGColor}") в указанном интервале Duration (Duration="0:0:0"). При переходе в состояние Normal, то есть, когда указатель мыши покидает элемент списка, анимация прерывается и внешняя рамка элемента списка принимаем исходный вид. XAML-разметка управления визуальным представлением элемент списка ListBoxItem для группы CommonStates приведена ниже.

    <VisualState x:Name="Normal" />
    <VisualState x:Name="MouseOver">
      <Storyboard>
        <ColorAnimation  Duration="0:0:0"
        To="{StaticResource PhotoSelectedBGColor}"
    	    Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" 
    	    Storyboard.TargetName="Border" />
      </Storyboard>
    </VisualState>

    Для группы SelectionStates определим функциональность для состояний Unselected и Selected. XAML-разметка управления визуальным представлением элемент списка ListBoxItem для группы SelectionStates спроектирована аналогично приведенной выше для группы CommonStates и имеет следующий вид.

    <VisualState x:Name="Unselected" />
    <VisualState x:Name="Selected">
      <Storyboard>
        <ColorAnimation Duration="0:0:0"
          To="{StaticResource ItemSelectedBGColor}"
          Storyboard.TargetProperty= (Border.Background).(SolidColorBrush.Color)"
          Storyboard.TargetName="Border" />
      </Storyboard>
    </VisualState>

    Стиль ListBoxPhotoStyle задается для типа ListBox и определяет элемент прокрутки ScrollViewer. Стиль элемент прокрутки задается ссылкой на статический ресурс ScrollViewerPhotoStyle.

    Стиль ScrollViewerPhotoStyle задается для типа ScrollViewer (TargetType="ScrollViewer") и в нем определяются четыре свойства: HorizontalScrollBarVisibility, VerticalScrollBarVisibility, BorderThickness и Template.

    <Style x:Key="ScrollViewerPhotoStyle" TargetType="ScrollViewer">
        <Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
        <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
        <Setter Property="BorderThickness" Value="0"/>
        <Setter Property="Template">
            <Setter.Value>
                     <!—Определение вложенного свойства ControlTemplate -->
            </Setter.Value>
        </Setter>
    </Style>

    Шаблон ControlTemplate содержит два контейнера Grid. Для внешней сетки (Grid) задается свойство ScrollContentPresenter, определяющее где и как будет отображаться содержимое элемента управления ScrollViewer. Вложенная сетка имеет две колонки.

    <ControlTemplate TargetType="ScrollViewer">
      <Grid>
        <ScrollContentPresenter 
    	x:Name="ScrollContentPresenter" 
    	Cursor="{TemplateBinding Cursor}" 
    	ContentTemplate="{TemplateBinding ContentTemplate}" 
    	Grid.Column="1" Grid.Row="1"/>
        <Grid>
           <Grid.ColumnDefinitions>
             <ColumnDefinition Width="*"/>
             <ColumnDefinition Width="Auto"/>
           </Grid.ColumnDefinitions>
                 <!—Определение свойства ScrollBar -->
        </Grid>
      </Grid>
    </ControlTemplate>

    Элемент управления ScrollBar предоставляет полосу прокрутки с перемещаемым элементом, позиция которого соответствует значению.

    <ScrollBar 
    	x:Name="VerticalScrollBar" 
    	Grid.Row="0" Grid.Column="1" 
    	IsTabStop="False" 
    	Maximum="{TemplateBinding ScrollableHeight}" 
    	Height="250" Minimum="0" 
    	Orientation="Vertical" 
    	VerticalAlignment="Center"
    	Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" 
    Value="{TemplateBinding VerticalOffset}" 
    ViewportSize="{TemplateBinding ViewportHeight}" 
     Width="18" Margin="10" HorizontalAlignment="Center"
    	Opacity="0.5"/>

    Шаблон данных ListBoxItemPhotoDataTemplate практически не отличается от ранее определенного шаблона для элемента listBoxEmployees, за исключением внешней и внутренней рамок. Полный код XAML-разметки шаблона данных ListBoxItemPhotoDataTemplate приведен в приложении А.

    Стили и шаблон для интерфейсного элемента ListBox поместим в словарь ресурсов StylesAndTemplates.xaml. Так как созданные стили ссылаются на ресурсы из файла ColorsAndBrushesRD.xaml, то данные ресурсы необходимо сделать доступными в файле StylesAndTemplates.xaml с помощью свойства MergedDictionaries словаря ResourceDictionary. Свойство MergedDictionaries представляет различные словари ресурсов в объединенных словарях.

    <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="ColorsAndBrushesRD.xaml"/>
        </ResourceDictionary.MergedDictionaries>

    После создания стилей и шаблон для интерфейсного элемента ListBox необходимо модифицировать XAML-разметку для элемента listBoxEmployees главного окна приложения.

    <ListBox Grid.Row="1" Name="listBoxEmployees" HorizontalAlignment="Center" 
    	Margin="45,2,76,9" Padding="3" Width="263" 
    	ItemContainerStyle="{StaticResource ListBoxItemStyle}"
    	Style="{StaticResource ListBoxPhotoStyle}" 
    	ItemTemplate="{StaticResource ListBoxItemPhotoDataTemplate}"
    	Background="{x:Null}" />

    Изменение визуального поведения элемента Button

    Создадим стиль ButtonStyle для типа Button (TargetType="Button") и в нем определяются шесть свойств: Background, Foreground, Padding, BorderThickness, BorderBrush и Template. Для свойств Background, Foreground, Padding и BorderThickness задаются конкретные значения, а для свойства BorderBrush определяется кисть градиентной заливки.

    Вложенное свойство ControlTemplate определено для типа Button (TargetType="Button"). Оно содержит контейнер Grid, для которого задается свойства Cursor (Cursor="Hand"). Свойство Cursor будет определять изменение вида курсора при наведении указателя мыши на кнопку. Вложенное свойство зависимостей VisualStateManager.VisualStateGroups элемента управления определяет визуальное представление элемента управления в различных состояниях. Рамка ( Border ) с именем Background определяет визуальное представление, которое будет изменяеться в различных состояниях кнопки. Свойство ContentPresenter определяет, что и как будет формироваться на панели кнопки. Три прямоугольника используются для представления внутреннего содержания кнопки в режимах получения фокуса ( FocusVisualElement ), наведения указателя мыши ( MouseOverVisualElement ) и в недоступном состоянии ( DisabledVisualElement ).

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

    Для группы состояний CommonStates в состоянии Normal применяется визуальное представление по умолчанию, а для состояний MouseOver, Pressed и Disabled задается как цветовая ( ColorAnimation ), так числовая ( DoubleAnimation ) анимация свойств кнопки.

    Для группы состояний FocusStates в состоянии Unfocused применяется визуальное представление по умолчанию, а для состояния Focused задается как цветовая анимация свойств кнопки – изменяется прозрачность.

    Стиль для интерфейсного элемента Buttun добавим в словарь ресурсов StylesAndTemplates.xaml. далее необходимо модифицировать XAML-разметку для элементов Buttun главного окна приложения. Задание стиля производится аналогично для всех кнопок главного окна приложения. Так для кнопки "Создать" ( ButtonNew ) XAML-разметка будет иметь следующий вид:

    <Button Name="ButtonNew" 
                  Content="Создать"  
                  Margin="5" 
                  Padding="10,5,10,5" 
                  Height="25"
                  Click="New_Click" 
                  ToolTipService.ToolTip="Создать новую запись"
                  IsEnabled="False" 
                  Style="{StaticResource ButtonStyle}" />

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

    (рис 9.10) Визуальное представление кнопки в различных состояниях
    Вернуться к учебному плану