Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Обзор решения TaskVision: архитектура и реализация
В статье описываются проектные и архитектурные решения в приложении-примере TaskVision, которое демонстрирует, как использовать классы Windows Forms инфраструктуры .NET Framework в сочетании с Web-сервисами XML для разработки интеллектуального клиентского приложения, управляющего задачами.

Аннотация

В статье описываются проектные и архитектурные решения в приложении-примере TaskVision, которое демонстрирует, как использовать классы Windows Forms инфраструктуры .NET Framework в сочетании с Web-сервисами XML для разработки интеллектуального клиентского приложения, управляющего задачами.

Скачайте код этого решения:
TaskVision Client
TaskVision Server
TaskVision Source

Содержание

Обзор

Что представляет собой решение TaskVision?

TaskVision - пример интеллектуального клиентского приложения для управления задачами, разработанный с использованием классов Windows Forms инфраструктуры Microsoft® .NET Framework, которая является неотъемлемой частью Windows® и применяется при разработке и выполнении нового поколения приложений и Web-сервисов XML. TaskVision позволяет аутентифицированным пользователям просматривать, изменять, добавлять проекты и задачи, над которыми они работают совместно с другими пользователями. Приложение можно использовать в самых разных целях - от отслеживания ошибок в проектах до управления нарядами на выполнение какой-либо работы или запросами на обслуживание заказчиков. Основное предназначение TaskVision - предоставить программистам для .NET Framework, интересующимся созданием интеллектуальных клиентских приложений и Web-сервисов XML, пример качественного исходного кода. На рис. 1 показан внешний вид приложения TaskVision.

Рис. 1. Интерфейс TaskVision

Решение TaskVision демонстрирует применение разнообразных технологий, доступных в .NET Framework:

  • моделей подключенного (online) и автономного (offline) приложения;
  • модели обновления приложения по протоколу HTTP (автоматическое развертывание);
  • авторизации - управления доступом пользователей к функциональности приложения;
  • обработки конфликтов данных;
  • печати и предварительного просмотра перед печатью;
  • поддержки тем Windows XP (Windows XP Themes);
  • динамических свойств;
  • поддержки локализации;
  • поддержки специальных возможностей (accessibility) (ограниченная);
  • аутентификации форм с хранением имен пользователей и паролей в базе данных;
  • асинхронных вызовов Web-сервисов XML;
  • доступа к данным через ADO.NET и хранимые процедуры SQL;
  • разработку с помощью GDI+;
  • интеграцию кода на основе .NET Framework с COM-приложениями (механизм COM Interop).

В данном документе детально рассматривается решение TaskVision и рассказывается о его архитектуре с точки зрения разработчиков этого решения. Кроме того, поясняется, как использовать TaskVision в качестве шаблона при разработке интеллектуальных клиентских приложений. Также обсуждаются основные возможности приложения и технологии, примененные при их реализации. Полную информацию о TaskVision см. по ссылке http://www.windowsforms.net/taskvision.

Решение TaskVision разработано в Microsoft® Visual Studio .NET™ на языках C# и Visual Basic .NET. Кроме того, TaskVision перенесено на платформу PocketPC. Информацию о том, как создавалась версия TaskVision для PocketPC, см. в статье Creating the Pocket TaskVision Application в MSDN.

Приступая к работе с TaskVision

Самый простой способ увидеть TaskVision в действии - скачать и установить TaskVision Live Client v1.0 MSI. Live Client содержит скомпилированный исполняемый файл и настроен на обмен данными с общедоступным Web-сервисом XML.

Для установки сервера, с которым взаимодействует клиент, предназначен TaskVision Server v1.1 MSI. При установке сервера создается база данных, устанавливаются Web-сервисы XML и Web-сайт, на котором размещается обновление клиента до версии 1.1, считываемое и выполняемое при автоматическом развертывании (каталоги http://localhost/TaskVisionWS и http://localhost/TaskVisionUpdates соответственно). Такая установка идеально подходит тем, кто собирается использовать все решение локально, без компиляции кода.

Для тех, кто хочет поработать с исходным кодом TaskVision, мы выпустили TaskVision Source Code v1.1 MSI, при установке которого создается база данных и устанавливается исходный код клиентского приложения версии 1.1 (версии, которая скачивается и устанавливается при автоматическом развертывании TaskVision Live Client) и исходный код Web-сервисов XML. Если у вас нет программного обеспечения, необходимого для установки этого MSI-пакета, вы можете посмотреть наиболее интересные фрагменты исходного кода в Интернете на странице TaskVision Source Code Viewer.

Ниже приведены системные требования и инструкции по установке MSI-пакетов.

Примечание TaskVision Server v1.1 MSI нельзя установить на тот же компьютер, что и TaskVision Source Code v1.1 MSI, так как оба установочных пакета используют одни и те же имена базы данных и виртуального каталога IIS.

Установка TaskVision Live Client

Скачайте и установите соответствующий MSI-пакет. После запуска приложения (из меню Start) вы увидите стандартное окно входа в систему, в котором нужно ввести имя пользователя "jdoe" и пароль "welcome". После входа вы можете по своему усмотрению изменять данные и исследовать функциональность приложения, но учтите, что все данные на общедоступном сервере каждую ночь сбрасываются, поэтому любые внесенные вами изменения на следующий день исчезнут.

Минимальные требования:

  • Windows 2000/XP или более поздней версии;
  • Microsoft .NET Framework 1.0;
  • Соединение с Интернетом по локальной сети/модему;
  • Microsoft Excel 2002 (рекомендуется, но не обязателен; используется для экспорта данных в Excel, если вы хотите посмотреть, как работает COM Interop).

Приложение не поддерживает клиенты ISA или Web-прокси.

Установка TaskVision Server v1.1

Перед установкой убедитесь, что выполняются следующие условия.

  • У вас есть привилегии администратора локального компьютера.
  • По умолчанию используется экземпляр SQL Server с именем "local" (именно такая конфигурация выбирается по умолчанию при установке SQL Server), а ваша учетная запись Windows обладает привилегиями администратора SQL Server при использовании системы защиты, интегрированной в базу данных.
  • Расширения файлов ASP.NET (.aspx и .asmx) зарегистрированы в IIS (Internet Information Services). (Если IIS установлен после установки .NET Framework, выполните команду "C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\aspnet_regiis.exe -i".)
  • Наконец, убедитесь, что SQL Server и IIS выполняются. Скачайте и установите соответствующий MSI-пакет. При этом будут установлены Web-сервисы XML, Web-сайт с обновлением решения и база данных решения. Перейдите в каталог с клиентским приложением TaskVision (устанавливается через TaskVision Live Client v1.0 MSI) и отредактируйте файл TaskVision.exe.config, как показано ниже (чтобы в нем указывались URL локального сервера). (Этот файл содержится в подкаталоге "1.0.0.0" и, возможно, в подкаталоге "1.1.0.0".)

    Примечание В примере ниже мы используем "localhost" в качестве имени сервера. Этот вариант годится на случай, когда и клиентское, и серверное приложения выполняются на одном компьютере. Если сервер и клиент работают на разных компьютерах, вместо "localhost" нужно указать URL компьютера, где выполняется сервер. Кроме того, если сервер и клиент запущены на разных компьютерах, следует внести те же изменения в файл TaskVision.exe.config, находящийся в подкаталоге "TaskVisionUpdates\1.1.0.0" каталога TaskVision Server.

    <appSettings>
    <!-- Здесь указываются настраиваемые пользователем параметры приложения и свойства.-->
    <!-- Пример: <add key="settingName" 
    	value="settingValue"/> -->
    <add key="AppUpdater1.UpdateUrl" 
    	value="http://localhost/taskvisionupdates/updateversion.xml"/>
    <add key="TaskVision.AuthWS.AuthService" 
    	value="http://localhost/taskvisionws/authservice.asmx"/>
    <add key="TaskVision.DataWS.DataService" 
    	value="http://localhost/taskvisionws/dataservice.asmx"/>
    </appSettings>
    </configuration>
    

Минимальные требования:

  • Windows 2000/XP или более поздней версии;
  • Microsoft .NET Framework 1.0;
  • IIS 5.0+;
  • SQL Server 2000.

Установка TaskVision Source Code v1.1 MSI

Перед установкой убедитесь, что выполняются следующие условия.

  • У вас есть привилегии администратора локального компьютера.
  • По умолчанию используется экземпляр SQL Server с именем "local" (именно такая конфигурация выбирается по умолчанию при установке SQL Server), а ваша учетная запись Windows обладает привилегиями администратора SQL Server при использовании системы защиты, интегрированной в базу данных.
  • Расширения файлов ASP.NET (.aspx и .asmx) зарегистрированы в IIS. (Если IIS установлен после установки .NET Framework, выполните команду "C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\aspnet_regiis.exe -i".)
  • Наконец, убедитесь, что SQL Server и IIS работают.

Скачайте и установите соответствующий MSI-пакет. При этом будут установлены Web-сервисы XML и база данных решения. Откройте решение TaskVision в Visual Studio .NET. Учтите, что если не установлен Excel 2002, решение не будет компилироваться до тех пор, пока вы не удалите или не закомментируете код, связанный с экспортом в Excel (в классе ExportExcel и классе главной формы).

После компиляции проекта и запуска приложения откроется стандартное окно входа, в котором нужно ввести имя пользователя "jdoe" и пароль "welcome".

Минимальные требования:

  • Windows 2000/XP или более поздней версии;
  • Visual Studio .NET 2002;
  • IIS 5.0+;
  • SQL Server 2000;
  • Microsoft Excel 2002 (рекомендуется).

Архитектура решения

Наше решение управления задачами состоит из трех основных компонентов: базы данных, Web-сервисов XML и интеллектуального клиентского приложения, разработанного на основе классов Windows Forms (рис. 2).

К базе данных могут обращаться только Web-сервисы XML, имеющие разрешения на выполнение хранимых процедур в базе данных. Ограничивая набор Web-сервисов XML, имеющих доступ к базе данных, мы гарантируем, что будут выполняться только определенные нами запросы к базе данных.

Web-сервисы XML, используемые в решении, предназначены для выполнения на общедоступном сервере, и любое приложение может обращаться к ним через Интернет. Конечно, Web-сервисы решения могут работать в интрасети компании, при этом доступ к данным ограничивается пределами внутренней сети. (Заметьте: к Web-сервисам XML могут обращаться любые приложения, имеющие доступ к серверу, на котором они размещаются, но для работы с сервисами нужны имя и пароль пользователя.)

Интеллектуальное клиентское приложение выполняет аутентификацию пользователей, передавая имя и пароль пользователя Web-сервису Authentication. При успешной аутентификации Web-сервис возвращает клиенту зашифрованный билет (ticket), который сохраняется клиентом и передается Web-сервису Data при каждом запросе данных. Web-сервис Data проверяет зашифрованный билет и обрабатывает запрос данных. (При разработке собственных решений имейте в виду: когда удостоверения или зашифрованные билеты, передаваемые через Web-сервисы XML, обеспечивают доступ к конфиденциальной информации либо к защищенным ресурсам или когда передаются сами конфиденциальные данные, мы рекомендуем использовать Secure Sockets Layer. SSL защитит данные от возможных атак.)

{Рисунок:
Удаленный пользователь, работающий с клиентским приложением
Имя пользователя и пароль
Зашифрованный билет
Зашифрованный билет и запрос данных
DataSet, содержащий данные
Web-сервис Authentication
Web-сервис Data
Web-сервер
Сервер базы данных
}

Рис. 2. Архитектура приложения TaskVision

База данных

Все разделяемые данные хранятся в базе данных SQL Server. К ним не относятся данные, специфичные для приложения, и параметры конфигурации. Это позволяет разработчикам создавать собственные приложения, каждое из которых считывает данные из своего уникального хранилища. Мы воспользовались этой возможностью при разработке версии TaskVision для .NET Compact Framework - Pocket TaskVision (выходит в марте), - которая использует ту же серверную функциональность. В этом разделе представлен обзор базы данных в решении TaskVision.

Схема базы данных

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

Рис. 3. Схема базы данных TaskVision

Хранимые процедуры

В решении TaskVision для инкапсуляции всех запросов к базе данных применяются хранимые процедуры. Хранимые процедуры позволяют провести четкую границу между базой данных и промежуточным уровнем доступа к данным. Это в свою очередь облегчает поддержку решения, так как изменения в схеме базы данных скрываются от компонентов доступа к данным. Кроме того, хранимые процедуры в определенных случаях позволяют добиться выигрыша в производительности, поскольку кэширование информации и ее частичная обработка в самой базе данных уменьшают число сетевых запросов.

Web-сервисы XML

Web-сервисы XML образуют основу среднего уровня решения и управляют аутентификацией и запросами данных любого взаимодействующего с ними клиентского приложения.

Мы решили реализовать предоставляемые средним уровнем возможности в двух Web-сервисах XML:

  • Authentication - получает текстовые удостоверения, содержащие информацию для входа; сервис можно настроить на выполнение под защитой SSL (но в рассматриваемом примере это пока не реализовано).
  • Data - после аутентификации принимает и передает не являющиеся критически важными данные без SSL. При практическом использовании решения можно было бы применять SSL для защиты Web-сервиса XML Data от возможных атак, связанных с обращением к сериализованным данным.

Web-сервис Authentication

Сервис Authentication (рис. 4) работает по очень простому принципу: проверяет по базе данных имя и пароль пользователя (с помощью хранимой процедуры) и возвращает уникальный зашифрованный билет (ticket), содержащий идентификатор пользователя. Если проверка имени и пароля пользователя терпит неудачу, то ничего не возвращается.

Значение билета кэшируется (в статическом объекте кэша Web-приложения) в течение двух минут после возвращения сервером. Это позволяет поддерживать на стороне сервера список недавно сгенерированных билетов, к которому может обращаться любой код, выполняемый в том же домене приложения (в дальнейшем вы увидите, как это делает сервис Data). Так как билеты хранятся в этом списке в течение двух минут, клиентским приложениям часто приходится повторно проходить аутентификацию, что обеспечивает защиту от "атак с воспроизведением пакетов" ("replay attacks"), когда злоумышленник, прослушивая сетевой трафик, получает билет и подменяет пользователя, вошедшего в систему.

Для создания билетов используется класс System.Web.Security.FormsAuthenticationTicket, который мы выбрали потому, что он позволяет помещать в билет данные (в нашем случае идентификатор пользователя).

Рис. 4 Аутентификация пользователей TaskVision

{Рисунок:
Запрос входа от AuthService.ASMX
Правильны ли имя и пароль пользователя?
Да
Значение билета на две минуты помещается в статический кэш
Возвращается билет
Нет
Ничего не возвращается (Null)
}
&

acute; Создаем билет
Dim ticket As New FormsAuthenticationTicket(userID, False, 1)
Dim encryptedTicket As String = FormsAuthentication.Encrypt(ticket)

´ Получаем значение периода ожидания в минутах
Dim configurationAppSettings As AppSettingsReader = New AppSettingsReader()
Dim timeout As Integer = _
   CInt(configurationAppSettings.GetValue("AuthenticationTicket.Timeout", _
   GetType(Integer)))

´ Кэшируем билет
Context.Cache.Insert(encryptedTicket, userID, Nothing, _
   DateTime.Now.AddMinutes(timeout), TimeSpan.Zero)

Web-сервис Data

Web-сервис XML Data позволяет клиентскому приложению считывать и изменять данные, а также с помощью сервиса Authentication проверять запросы пользователя.

Оба Web-сервиса XML выполняются в одном и том же домене приложения (в данном случае в одном и том же Web-приложении IIS), что позволяет сервису Data обращаться к кэш-памяти, используемой сервисом Authentication для хранения копий действительных билетов аутентификации.

При вызове любого открытого Web-метода сервиса Data требуется передавать билет аутентификации. Перед возвратом данных проверяется, присутствует ли этот билет в кэше. Если да, значит, пользователь успешно прошел аутентификацию в течение последних двух минут; в противном случае билет недействителен или устарел. В качестве дополнительной меры предосторожности мы извлекаем из билета идентификатор пользователя и проверяем содержащуюся в базе данных информацию о пользователе с этим идентификатором, чтобы выяснить, не заблокирован ли он (другим администратором) и обладает ли он привилегиями администратора TaskVision, дающими права на действия, разрешенные только администраторам.

Private Function IsTicketValid(ByVal ticket As String, ByVal IsAdminCall _
   As Boolean) As Boolean
   If ticket Is Nothing OrElse Context.Cache(ticket) Is Nothing Then
      ´ Не прошел аутентификацию
      Return False
   Else
      ´ Проверяем авторизацию пользователя
      Dim userID As Integer = _
         CInt(FormsAuthentication.Decrypt(ticket).Name)

      Dim ds As DataSet
      Try
         ds = SqlHelper.ExecuteDataSet(dbConn, "GetUserInfo", userID)
      Finally
         dbConn.Close()
      End Try

      Dim userInfo As New UserInformation()
      With ds.Tables(0).Rows(0)
         userInfo.IsAdministrator = CBool(.Item("IsAdministrator"))
         userInfo.IsAccountLocked = CBool(.Item("IsAccountLocked"))
      End With

      If userInfo.IsAccountLocked Then
         Return False
      Else
         ´ При вызовах, требующих привилегий администратора, проверяем,
         ´ является ли пользователь администратором
         If IsAdminCall And Not userInfo.IsAdministrator Then
            Return False
         End If

         Return True
      End If
   End If
End Function
<WebMethod()> _
Public Function GetTasks(ByVal ticket As String, ByVal projectID _
   As Integer) As DataSetTasks

   ´ Если билет недействителен, возвращаем пустое значение
   If Not IsTicketValid(ticket) Then Return Nothing
   
   Dim ds As New DataSetTasks()
   daTasks.SelectCommand.Parameters("@ProjectID").Value = projectID
   daTasks.Fill(ds, "Tasks")
   Return ds
End Function

Интеллектуальный клиент Windows Forms

Интеллектуальное клиентское приложение - самая заметная часть решения, поскольку именно оно является инструментом, с помощью которого конечные пользователи управляют проектами и задачами. Как уже упоминалось, TaskVision предназначено для демонстрации ряда ключевых технологий и сценариев применения интеллектуальных клиентов. Многие из таких технологий и сценариев мы рассмотрим в дальнейшем. Полезным дополнением к этому разделу является страница TaskVision Source Code Viewer, на которой дан подробный анализ наиболее интересных фрагментов исходного кода.

Формы пользовательского интерфейса

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

На форме Login (рис. 5) пользователь аутентифицируется, вводя свои удостоверения для TaskVision. (По умолчанию для входа в приложение используются имя "jdoe" и пароль "welcome".)

Рис. 5 Форма входа в TaskVision

На главной форме (рис. 6) показываются данные, возвращаемые Web-сервисом Data (или получаемые из автономных файлов). Эта форма - фундамент нашего событийно-управляемого приложения; пользователь работает с приложением в основном через эту форму. Основными элементами управления формы являются главное меню, панель инструментов с кнопками, несколько пользовательских панелей, содержащих поля со списками, диаграммы и ссылки, DataGrid, панель предварительного просмотра перед печатью, два элемента управления Splitter (разделитель) и строка состояния, показывающая текущую информацию - количество записей и состояние подключения.

Большую часть формы занимает элемент управления DataGrid, где показываются итоговые данные о содержащихся в базе данных задачах, которые относятся к выбранному проекту. Под DataGrid выводится более подробная информация о выбранной задаче. Слева от DataGrid расположены элементы управления, позволяющие выбрать другой проект и фильтровать отображаемые задачи, а под этими элементами управления находятся две диаграммы GDI+ (GDI+ charts) со сведениями о задачах. Под диаграммами показывается элемент управления, содержащий хронологию работы над выбранной задачей - информацию о создании задачи и вносимых в нее изменениях. Вверху формы расположены меню и кнопки, позволяющие управлять пользователями TaskVision, переключать языки, создавать новые проекты и задачи, экспортировать отображаемую в DataGrid информацию в Excel, выбирать работу в автономном или онлайновом (подключенном) режиме.

Рис. 6. Главная форма TaskVision

При двойном щелчке задачи в DataGrid, показываемом на главной форме, открывается форма Task (рис. 7). На ней размещены элементы управления, позволяющие изменять данные о задаче: дату выполнения, какой сотрудник выполняет задачу, приоритет, краткое и подробное описания задачи, на сколько процентов она выполнена. Кроме того, у этой формы еще одно применение - на вкладке History показывается хронология выполнения задачи (рис. 8). Заметим, что та же форма Task выводится, когда пользователь добавляет задачу, щелкая кнопку New Task на главной форме или выбирая New Task в меню File.

Рис. 7. Форма Task

Рис. 8. Форма Task, вкладка History

Форма Manage Users (рис. 9) вызывается при выборе Users в меню Manage главной формы и содержит информацию обо всех пользователях приложения. При щелчке кнопки Edit показывается форма Edit User (рис. 10), где можно изменить данные о пользователе. Учтите, что меню Manage доступно только тем пользователям, которые вошли в TaskVision как администраторы.

Рис. 9. Форма Manage Users

Рис. 10. Форма Edit User

Форма Change Password (рис. 11) вызывается при выборе Change Password в меню File и используется для смены пароля текущего пользователя. Для вызова формы не нужны привилегии администратора TaskVision, но она доступна только в онлайновом режиме.

Рис. 11. Форма Change Password

Форма Search (рис. 12) появляется при выборе Search в меню View или щелчке кнопки Search вверху главной формы и используется для простого поиска подстроки во всех задачах текущего проекта и вывода результатов поиска.

Рис. 12. Форма Search

Форма Customize Columns (рис. 13) выводится при выборе Customize Columns в меню View и используется для настройки TableStyle элемента управления DataGrid, что дает возможность выбирать поля, показываемые в сетке, и их внешний вид.

Рис. 13. Форма Customize Columns

Форма Add Project (рис. 14) открывается при выборе Add Project в меню Manage и позволяет администраторам TaskVision добавлять проекты в удаленную базу данных.

Рис. 14. Форма Add Project

Компонент доступа к данным

Класс DataLayer является оболочкой Web-сервисов XML и диспетчером данных нашего клиентского приложения.

С прикладной точки зрения, у нас имеются видимая структура и шаблон проектирования, управляющий обработкой данных. На рис. 15 показаны отношения "объект-владелец", в которых участвуют класс DataLayer и классы форм.

Когда в главной форме обрабатываются события, например при открытии формы Search, объект DataLayer передается новой форме, что обеспечивает доступ к тем же данным, к которым вправе обращаться главная форма.

{Рисунок:
Главная форма
Класс DataLayer
Форма Task
Форма Search
Прочие формы…
AuthService
DataService
Класс информации о текущем пользователе
DataSet задач
DataSet справочных таблиц
DataSet проектов
Билет аутентификации
Имя пользователя
Идентификатор пользователя
Пароль пользователя
Прочая информация о пользователе
}

Рис. 15. Отношения "объект-владелец" в иерархии классов TaskVision

Информация о проекте, задачах, пользователях и вся остальная информация, возвращаемая Web-сервисом XML, хранится в классе DataLayer. Для доступа к этим данным используются открытые члены класса DataLayer, а различные UI-формы могут считывать и изменять эти локальные данные. Изменение и считывание данных Web-сервиса XML может выполняться только с помощью открытых методов класса DataLayer. К этим открытым методам относятся: GetProjects, GetTasks и UpdateTasks.

Класс DataLayer предназначен для работы в однопоточной среде и, вызывая эти методы из главного потока, мы можем быть уверены, что информация, получаемая при вызове Web-сервиса XML, правильно считывается в локальные данные в синхронном режиме и что наши элементы управления, связываемые с данными, не обновляются в фоновом потоке.

Большинство открытых методов (в том числе показанный ниже код) написано по следующему принципу. Запрос данных к Web-сервису Data (или же передача ему данные) выполняется с использованием текущего билета аутентификации, затем проводятся повторная аутентификация и (при необходимости) обработка исключений. Возвращаемые данные объединяются с локальными, и вызывающему коду возвращается DataLayerResult, показывающий, успешно или неудачно завершилась операция.

Public Function GetProjects() As DataLayerResult
   ´ Это DataSet, возвращаемый Web-сервисом
   Dim ds As DataSetProjects
   Try
      ´ Запрашиваем DataSet и передаем билет
       ds = m_WsData.GetProjects(m_Ticket)

       ´ Чтобы сообщить о просроченном билете, Web-сервисы TaskVision
       ´ возвращают пустое значение (или -1 при запросе целых значений)
       If ds Is Nothing Then
          ´ Получаем новый билет и повторно выполняем вызов
          Dim ticketResult As DataLayerResult = GetAuthorizationTicket()
         ´ Если получить билет неудалось, возвращаем
         ´ полученное значение, описывающее ошибку
          If ticketResult <> DataLayerResult.Success Then
             Return ticketResult
          End If

         ´ Повторяем вызов
          ds = m_WsData.GetProjects(m_Ticket)

          ´ Следующий блок кода никогда не должен выполняться.
          ´ Его выполнение означает, что билет устарел слишком быстро.
          If ds Is Nothing Then
             Return DataLayerResult.AuthenticationFailure
          End If
       End If
   Catch ex As Exception
      Return HandleException(ex)
    End Try

    DsProjects.Clear()
    DsProjects.Merge(ds)
    Return DataLayerResult.Success
End Function
Public Enum DataLayerResult
    None = 0
    Success = 1 
    ServiceFailure = 2
    UnknownFailure = 3
    ConnectionFailure = 4
    AuthenticationFailure = 5
End Enum

Значения перечислимого DataLayerResult имеют следующий смысл.

  • DataLayerResult.Success означает, что открытый метод успешно выполнил свою задачу.
  • DataLayerResult.ServiceFailure означает, что при выполнении Web-сервиса XML в его коде сгенерировано исключение.
  • DataLayerResult.ConnectionFailure означает, что возникла проблема при соединении с Web-сервисом XML (возможно, связанная с локальным Интернет-соединением или отсутствием ответа Web-сервиса XML).
  • DataLayerResult.AuthenticationFailure возвращается, когда текущие имя и пароль пользователя (вводимые в форме Login) более недействительны.

До настоящего момента все описываемые вызовы Web-сервиса XML выполнялись синхронно в основном потоке приложения. Кроме того, в приложении есть два примера реализации асинхронных вызовов Web-сервиса XML (т. е. вызовов, выполняемых в потоке, отличном от основного). Это позволяет работать с приложением как обычно, в то время как оно ожидает завершения выполняемого в фоновом режиме асинхронного вызова Web-сервиса.

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

В нашей главной форме имеется таймер, периодически вызывающий обновление хронологической информации. Метод BeginGetProjectHistory возвращает объект IAsyncResult, который проверяется в последующих событиях таймера до тех пор, пока метод не завершится. После завершения метода вызывается метод EndGetProjectHistory, объединяющий полученные данные с локальными аналогично рассмотренным ранее синхронным методам.

Public Function BeginGetProjectHistory(ByVal projectID As Integer) As IAsyncResult
   Try
      ´ Примечание: предполагается, что наш билет всегда действителен,
      ´ так как этот метод вызывается сразу после запроса проекта или задачи

      ´ Инициируем асинхронный вызов
      Return m_WsData.BeginGetProjectHistory(m_Ticket, projectID, _
         Nothing, New Object() {projectID})

   Catch ex As Exception
      LogError.Write(ex.Message & vbNewLine & ex.StackTrace)
      Return Nothing
   End Try
End Function

Public Function EndGetProjectHistory(ByVal ar As IAsyncResult) As DataLayerResult
   Dim ds As DataSetProjectHistory

   Try
      ´ Получаем новый DataSet
      ds = m_WsData.EndGetProjectHistory(ar)

      If ds Is Nothing Then
         Return DataLayerResult.AuthenticationFailure
      End If
   Catch ex As Exception
      Return HandleException(ex)
   End Try

   DsProjectHistory.ProjectHistory.Clear()
   DsProjectHistory.Merge(ds)
   Return DataLayerResult.Success
End Function

Второй пример асинхронного вызова Web-сервиса XML в решении TaskVision - обновление выводимого в DataGrid на главной форме объекта DataSet текущей информацией о задачах.

В главной форме есть еще один таймер, используемый при заполнении DataSet текущей информацией о задачах. Единственное существенное отличие от предыдущего примера в том, когда выполняется асинхронный вызов. В отличие от периодического или вынужденного вызова Web-сервиса XML, этот таймер постоянно остановлен и сбрасывается, когда пользователь изменяет данные. Когда приложение достаточно долго работает вхолостую и таймер успевает сработать, запускается асинхронный запрос и затем с каждым срабатыванием таймера проверяется, завершилось ли выполнение запроса. Если запрос выполнен, данные объединяются с локальными данными. Если пользователь внес какие-либо изменения (обновил данные) во время выполнения асинхронного запроса, запрос отменяется как устаревший.

Конфликты данных

Существует несколько способов обработки конфликтов данных. Типичный пример конфликта - пользователь пытается обновить или удалить данные, которые изменились с того момента, когда он последний раз их считывал, или которых больше нет. Зачастую в таких ситуациях генерируется ошибка, или же данные, вводимые пользователем, перезаписывают данные, хранящиеся в базе. В первом случае пропадает работа, проделанная пользователем. Второй вариант чреват тем, что важные данные, введенные после того, как клиент последний раз прочитал данные из базы, перезаписываются или стираются. TaskVision предлагает простое решение этой проблемы, основанное в значительной мере на возможностях объектов DataSet библиотеки ADO.NET в .NET Framework. (DataSet - это объект, содержащий кэш информации, выбранной из базы данных.)

Чтобы с помощью DataSet управлять задачами в TaskVision, мы решили использовать класс SQLDataAdapter из пространства имен System.Data.SqlClient, позволяющий инкапсулировать в одном объекте выборку, обновление, вставку и удаление данных. Метод Update класса DataAdapter проверяет RowState каждого DataRow, входящего в состав DataSet, определяет, содержит ли объект DataRow новую, измененную или удаленную запись, и выполняет соответствующую хранимую процедуру. Потом класс DataAdapter проверяет, больше ли нуля число измененных записей в базе данных. (Если при операциях обновления и удаления количество обновленных записей равно нулю, значит, хранимая процедура потерпела неудачу, не найдя нужных данных.)

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

CREATE PROCEDURE [UpdateTask]
(
   @TaskID int,
   @ProjectID int,
   @ModifiedBy int,
   @AssignedTo int,
   @TaskSummary varchar(70),
   @TaskDescription varchar(500),
   @PriorityID int,
   @StatusID int,
   @Progress int,
   @IsDeleted bit,
   @DateDue datetime,
   @DateModified datetime,
   @DateCreated datetime,
   @Original_ProjectID int,
   @Original_ModifiedBy int,
   @Original_AssignedTo int,
   @Original_TaskSummary varchar(70),
   @Original_TaskDescription varchar(500),
   @Original_PriorityID int,
   @Original_StatusID int,
   @Original_Progress int,
   @Original_IsDeleted bit,
   @Original_DateDue datetime,
   @Original_DateModified datetime,
   @Original_DateCreated datetime
)
AS
SET NOCOUNT OFF;
-- Заметьте: при сравнении дат мы используем приведение к типу
-- varchar с помощью convert, что позволяет использовать эту процедуру
-- в приложении для Pocket Pc, которое при работе в автономном режиме
-- поддерживает только четырехбайтовые поля datetime.

UPDATE Tasks 
SET ProjectID = @ProjectID, ModifiedBy = @ModifiedBy, AssignedTo = @AssignedTo, 
	TaskSummary = @TaskSummary, TaskDescription = @TaskDescription, 
	PriorityID = @PriorityID, StatusID = @StatusID, Progress = @Progress, 
	IsDeleted = @IsDeleted, DateDue = @DateDue, DateModified = @DateModified 
WHERE (TaskID = @TaskID) AND (ProjectID = @Original_ProjectID) AND 
	(ModifiedBy = @Original_ModifiedBy) AND (AssignedTo = @Original_AssignedTo) AND 
	(TaskSummary = @Original_TaskSummary) AND 
	(TaskDescription = @Original_TaskDescription) AND 
	(ProjectID = @Original_ProjectID) AND (StatusID = @Original_StatusID) AND 
	(Progress = @Original_Progress) AND (IsDeleted = @Original_IsDeleted) AND 
	(convert(varchar(20), DateDue) = convert(varchar(20), @Original_DateDue)) AND 
	(convert(varchar(20), DateModified) = convert(varchar(20), 
	@Original_DateModified)) AND 
	(convert(varchar(20), DateCreated) = convert(varchar(20), 
	@Original_DateCreated)) AND 
	(PriorityID = @Original_PriorityID);
SELECT TaskID, ProjectID, ModifiedBy, AssignedTo, TaskSummary, TaskDescription, PriorityID, 
	StatusID, Progress, IsDeleted, DateDue, DateModified, DateCreated 
FROM Tasks WHERE (TaskID = @TaskID)
GO

Как видно из текста хранимой процедуры, в разделе WHERE весьма тщательно проверяется, не возник ли конфликт данных. Вас может заинтересовать, откуда берутся "исходные" значения, используемые в разделе WHERE при выявлении конфликтов. По умолчанию в каждом DataRow класса DataSet хранятся и исходное значение, прочитанное из базы данных при первоначальном создании DataSet, и текущее значение, изменяемое пользователем.

Когда возвращается нулевое число измененных записей, SQLDataAdapter прекращает обновление и генерирует исключение DBConcurrencyException. Это исключение мы и обрабатываем при разрешении конфликтов данных.

Как показано в приведенном ниже фрагменте кода, мы выполняем цикл (зачем - мы объясним позже), и, если других исключений не возникло, выходим из цикла. Если приходится перехватывать исключение DBConcurrencyException, мы сначала пытаемся прочитать запись задачи (по TaskID) и определить, существует ли запись в базе данных или она удалена. Удаление записи обрабатывается весьма просто: так как клиентское приложение не может физически удалять записи, ее мог удалить только системный администратор. Поскольку действия системного администратора приоритетны, клиенту остается лишь смириться с потерей записи и внесенных изменений. Цикл используется только для того, чтобы потом можно было удалить DataRow и повторно выполнить обновление без выхода из метода и возврата управления клиенту (так как этот код выполняется Web-сервисом XML). Если запись все еще есть, значит, она не соответствует условию раздела WHERE и следует дать пользователю решить, что с ней делать. Теперь нужно показать пользователю внесенные им изменения и новые значения, хранящиеся в базе данных.

Исходные значения, которые, как полагал пользователь, раньше содержались в базе данных, больше не представляют интереса, поскольку другой пользователь заменил эти значения на новые. Учитывая этот факт и то, что DataRow может содержать два набора значений, мы создаем копию изменений, применяем новые значения и снова копируем изменения в DataRow. Теперь DataRow содержит последнюю версию записи, полученную из базы данных, и изменения, которые хочет внести пользователь. Прежние значения заменяются текущими значениями из базы данных. После этого Web-сервис возвращает DataSet клиентскому приложению TaskVision, которое показывает пользователю сообщение об ошибке (рис. 16). Приложение отображает форму, предназначенную для разрешения конфликта данных. На ней выводятся два набора значений, и пользователь может выбрать одно из двух: сохранить изменения или отменить свое действие, оставив те значения, которые сейчас хранятся в базе данных.

Рис. 16. Форма Conflict resolution

´ Вызываем метод обновления в цикле, из которого выходим при успешном 
´ обновлении; возможен также преждевременный выход при конфликте данных.
´ Использование цикла позволяет обрабатывать ситуацию с отсутствием записи,
´ не возвращая управление клиенту.
Do
   Try
      ´ Пытаемся обновить базу данных
      daTasks.Update(dsTasks, "Tasks")
      Exit Do ´ в большинстве случаев выполняется этот оператор
   Catch dbEx As DBConcurrencyException
      ´ Это исключение генерируется либо из-за того, что другой пользователь
      ´ изменил запись, либо из-за того, что запись удалена администратором
      ´ базы данных; сначала пытаемся получить обновленную запись.
      Dim ds As New DataSet()
      Dim cmd As New SqlCommand("GetOneTask", dbConn)
      cmd.CommandType = CommandType.StoredProcedure
   
      ´ Получаем обновленную запись
      Dim da As New SqlDataAdapter(cmd)
      da.SelectCommand.Parameters.Add("@TaskID", dbEx.Row.Item("TaskID"))
      da.Fill(ds)
   
      ´ Если запись все еще существует…
      If ds.Tables(0).Rows.Count > 0 Then
         Dim proposedRow As DataRow = dbEx.Row.Table.NewRow()
         Dim databaseRow As DataRow = ds.Tables(0).Rows(0)
   
         ´ …создаем копию изменений, которые пытается внести пользователь
         proposedRow.ItemArray = dbEx.Row.ItemArray
   
         ´ Заносим в запись значения из базы данных
         ´ и повторно применяем предполагаемые изменения
         With dbEx.Row
            .Table.Columns("TaskID").ReadOnly = False
            .ItemArray = databaseRow.ItemArray
            .AcceptChanges()
            .ItemArray = proposedRow.ItemArray
            .Table.Columns("TaskID").ReadOnly = True
         End With
   
         ´ Примечание: так как эта запись вызвала исключение ADO.NET, она
         ´ помечается свойством rowerror, которое мы оставляем без изменений
         ´ при передаче DataSet клиентскому приложению
         Return dsTasks
      Else
         ´ Если запись удалена кем-то еще, приоритет отдается ему
         dbEx.Row.Delete()
         dbEx.Row.AcceptChanges()
      End If
   End Try
Loop

Модель "подключенные-отсоединенные данные"

В подключенном режиме клиентское приложение TaskVision хранит все данные в памяти и при каждом изменении, внесенном пользователем, обращается к Web-сервису XML для проверки допустимости этого изменения и его применения. Но клиентское приложение TaskVision поддерживает и автономный (отключенный) режим.

Автономный режим выбирается пользователем (щелчком соответствующей кнопки на панели управления). При переключении в этот режим выполняется целый ряд действий:

  • Во-первых, объекты DataSet сохраняются в формате XML на локальном жестком диске. Важно понимать, что с этого момента данные отражают последнее известное состояние базы данных.
  • Во-вторых, глобальному объекту типа Boolean присваивается значение false, после чего приложение прекращает отправлять изменения Web-сервису XML (так как данные хранятся локально, пока пользователь не попытается вернуться в подключенный режим).
  • И, в-третьих, меняется GUI, отражая автономный режим работы.

В автономном режиме изменения сохраняются в объектах DataSet, а измененные DataRow помечаются как "Changed".

Если пользователь закрывает приложение, находящееся в автономном режиме, измененные DataRow записываются на диск в отдельный XML-файл (три исходных объекта DataSet - Projects, Tasks и LookupTables, - как упоминалось ранее, уже сохранены).

Если при повторном запуске приложения обнаруживаются отсоединенные данные, считается, что в последний раз оно работало в автономном режиме. Основные объекты DataSet заполняются информацией из XML-файлов, затем проверяется наличие файла изменений (the change file) (метод AcceptChanges для этого XML-файла не вызывается). Так как метод AcceptChanges не вызывается, эти записи остаются помеченными как "Changed", благодаря чему приложения показывает те же данные, что и раньше (до выхода пользователя из приложения).

Try
    ´ Проверяем существование файлов с отсоединенными данными
    If File.Exists(m_MyDocumentsPath & c_OfflineTasksFile) AndAlso _
      File.Exists(m_MyDocumentsPath & c_OfflineProjectsFile) AndAlso _
      File.Exists(m_MyDocumentsPath & c_OfflineLookUpTablesFile) Then
        Try
            ´ Устанавливаем автономный режим
            ChangeOnlineStatus(False)

            ´ Пытаемся считать отсоединенные данные
            m_DataLayer.DsProjects.ReadXml(m_MyDocumentsPath & _
         c_OfflineProjectsFile, XmlReadMode.ReadSchema)

            m_DataLayer.DsTasks.ReadXml(m_MyDocumentsPath & _
         c_OfflineTasksFile, XmlReadMode.ReadSchema)

            m_DataLayer.DsLookupTables.ReadXml(m_MyDocumentsPath & _
         c_OfflineLookUpTablesFile, XmlReadMode.ReadSchema)

            ´ Обходной путь: в схеме отсутствует описание того, что поле 
            ´ является автоинкрементным
            m_DataLayer.DsTasks.Tasks.Columns("TaskID").AutoIncrement = True

            ´ Теперь данные стали такими же, как на момент перехода
            ´ в автономный режим
            m_DataLayer.DsTasks.AcceptChanges()

            ´ Если изменения есть, считываем их
            If File.Exists(m_MyDocumentsPath & c_OfflineTaskChangesFile) Then
                m_DataLayer.DsTasks.ReadXml(m_MyDocumentsPath & _
         c_OfflineTaskChangesFile, XmlReadMode.DiffGram)
            End If

            ´ Так как в реестре сохраняется идентификатор последнего
            ´ проекта, с которым работал пользователь, пытаемся найти запись
            ´ для этого проекта; если это не удается, выбираем первый проект
            If m_DataLayer.DsProjects.Projects.Rows.Find(m_ProjectID) _
            Is Nothing Then

                m_ProjectID = _
               CType(m_DataLayer.DsProjects.Projects.Rows(0)("ProjectID"), _
               Integer)

            End If
        Catch ex As Exception
            LogError.Write(ex.Message & vbNewLine & ex.StackTrace)
            ´ Суть ошибки нас не интересует, просто сообщаем о ней
            ´ и продолжаем выполнение
            Dim mbResult As DialogResult = _
            MessageBox.Show(m_ResourceManager.GetString( _
            "MessageBox.Show(There_was_an_error_reading_theoffline_files)") _
            & vbNewLine & vbNewLine & _
            m_ResourceManager.GetString("Do_you_want_to_go_online"), _
            "", MessageBoxButtons.YesNo, MessageBoxIcon.Error, _
            MessageBoxDefaultButton.Button1, _
            MessageBoxOptions.DefaultDesktopOnly)

            Me.Refresh()
            If mbResult = DialogResult.Yes Then
                ´ Пользователь выбрал переход в подключенный режим
                ChangeOnlineStatus(True)

                DeleteOfflineFiles()
                m_DataLayer.DsProjects.Clear()
                m_DataLayer.DsTasks.Clear()
                m_DataLayer.DsLookupTables.Clear()
            Else
                Throw New ExitException()
            End If
        End Try
    End If

В дальнейшем, как только пользователь выберет возврат в подключенный режим, DataSet с информацией о задачах передается Web-сервису Data, и все изменения обрабатываются как обычно (в конечном счете результаты отправляются обратно клиентскому приложению). Если удалось соединиться с Web-сервисом XML, глобальному логическому объекту присваивается true и снова становится возможным выполнение запросов к этому сервису.

Компонент .NET Updater

.NET Application Updater Component позволяет клиентским приложениям, выполняемым в .NET Framework, автоматически обновлять себя, загружая новые версии сразу после их появления на удаленном Web-сервере.

Чтобы все это работало, нужны две вещи: вспомогательный исполняемый файл (заглушка) и компонент, встраиваемый в само клиентское приложение.

Исполняемый файл-заглушка, AppStart.exe, запускает соответствующую версию приложения TaskVision. (Обратите внимание, что ярлык, создаваемый при установке клиента, указывает на AppStart.exe, а не на TaskVision.exe.)

Файл-заглушка считывает локальный файл конфигурации, AppStart.config, чтобы определить местонахождение последней версии клиентского приложения. Затем создается новый процесс, запускающий TaskVision.exe из каталога, заданного в файле конфигурации. После запуска TaskVision.exe в этом новом процессе файл-заглушка просто ждет, когда процесс завершится.

При выполнении клиентского приложения TaskVision компонент .NET Updater работает в фоновом режиме и определяет, доступно ли обновление. Если появилась новая версия, компонент загружает обновление и перенастраивает файл-заглушку на запуск обновленной версии вместо исходной.

Для этого компонент проверяет XML-файл, хранящийся на сервере, - UpdateVersion.xml. Если номер версии в этом файле больше номера версии локального приложения TaskVision, то компонент по пути, заданному в UpdateVersion.xml, устанавливает местонахождение файлов новой версии, создает локальный каталог и скачивает в него файлы новой версии. Далее компонент вносит изменения в локальный файл конфигурации, переключая заглушку на запуск новой версии исполняемого файла из этого каталога (таким образом, при следующем запуске заглушки загружается последняя версия приложения).

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

Стили столбцов DataGrid

Класс DataGrid библиотеки Windows Forms - готовый элемент управления, показывающий информацию в виде электронной таблицы (типичный вид DataGrid показан на рис. 17).

Рис. 17. Класс DataGrid

С помощью табличных стилей разработчики могут настраивать вид и функции каждого столбца DataGrid. Чтобы усовершенствовать UI, мы создали три нестандартных класса столбцов и применили их к TableStyle.

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

Чтобы предотвратить это, наш класс DataGridTextBoxColumn (производный от .NET Framework-класса DataGridTextBoxColumn) переопределяет один из Edit-методов базового класса, делая невозможным получение фокуса ячейкой и копирование текста.

Protected Overloads Overrides Sub Edit(ByVal source As _
   System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, _
   ByVal bounds As System.Drawing.Rectangle, ByVal isReadOnly As _
   Boolean, ByVal instantText As String, ByVal cellIsVisible As Boolean)

      ´ Ничего не делаем
End Sub

Следующим был класс DataGridPriorityColumn, показывающий в DataGrid значки приоритета. Класс DataGridPriorityColumn написан в предположении, что значением поля является имя показываемого .gif-файла и что этот файл существует в каталоге изображений, используемых приложением.

Protected Overloads Overrides Sub Paint(ByVal g As System.Drawing.Graphics, _
   ByVal bounds As System.Drawing.Rectangle, ByVal source As _
   System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, _
   ByVal backBrush As System.Drawing.Brush, ByVal foreBrush As _
   System.Drawing.Brush, ByVal alignToRight As Boolean)
   Dim bVal As Object = GetColumnValueAtRow(source, rowNum)
   Dim imageToDraw As Image

   ´ Кэшируем изображения в хэш-таблице
   If m_HtImages.ContainsKey(bVal) Then
      imageToDraw = CType(m_HtImages(bVal), System.Drawing.Image)
   Else
      ´ Читаем изображение с диска и кэшируем изображение
      Try
         imageToDraw = Image.FromFile(c_PriorityImagesPath & _
            CType(bVal, String) & ".gif")
         m_HtImages.Add(bVal, imageToDraw)
      Catch
            ´ Показываем сообщение об ошибке
         Return
      End Try
   End If

   ´ Если запись является текущей, рисуем выделяющий прямоугольник
   If Me.DataGridTableStyle.DataGrid.CurrentRowIndex = rowNum Then
      g.FillRectangle(New    SolidBrush(Me.DataGridTableStyle.SelectionBackColor), _
         bounds)
   Else
      g.FillRectangle(backBrush, bounds)
   End If

   ´ Выводим изображение
   g.DrawImage(imageToDraw, New Point(bounds.X, bounds.Y))
End Sub

Последний класс - DataGridProgressBarColumn - отображает индикатор прогресса, показывающий, на сколько процентов выполнена задача (связывается с полем Progress DataTable). При реализации класса мы используем объект Graphics, передаваемый в метод Paint, для вывода в соответствии со значением поля закрашенного прямоугольника и строкового представления значения (например, "75%").

Protected Overloads Overrides Sub Paint(ByVal g As System.Drawing.Graphics, _
   ByVal bounds As System.Drawing.Rectangle, ByVal source As _
   System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, _
   ByVal backBrush As System.Drawing.Brush, ByVal foreBrush As _
   System.Drawing.Brush, ByVal alignToRight As Boolean)

   Dim progressVal As Integer = CType(GetColumnValueAtRow(source, rowNum), Integer)
   Dim percentage As Single = CType((progressVal / 100), Single)

   ´ Если запись является текущей, рисуем выделяющий прямоугольник
   If Me.DataGridTableStyle.DataGrid.CurrentRowIndex = rowNum Then
       g.FillRectangle(New SolidBrush(Me.DataGridTableStyle.SelectionBackColor), _
       bounds)
   Else
       g.FillRectangle(backBrush, bounds)
   End If

   If percentage > 0.0 Then
      ´ Выводим прямоугольник и текст, информирующие о ходе выполнения
      g.FillRectangle(New SolidBrush(Color.FromArgb(163, 189, 242)), _
         bounds.X + 2, bounds.Y + 2, Convert.ToInt32((percentage * _
         bounds.Width - 4)), bounds.Height - 4)

      g.DrawString(progressVal.ToString() & "%", _
      Me.DataGridTableStyle.DataGrid.Font, foreBrush, bounds.X + 6, _
         bounds.Y + 2)
   Else
      ´ Выводим текст
      If Me.DataGridTableStyle.DataGrid.CurrentRowIndex = rowNum Then
         g.DrawString(progressVal.ToString() & "%", _
            Me.DataGridTableStyle.DataGrid.Font, New _
            SolidBrush(Me.DataGridTableStyle.SelectionForeColor), _
            bounds.X + 6, bounds.Y + 2)
      Else
         g.DrawString(progressVal.ToString() & "%", _
            Me.DataGridTableStyle.DataGrid.Font, foreBrush, _
            bounds.X + 6, bounds.Y + 2)
      End If
End If
End Sub

Печать и предварительный просмотр

Создать документ для печати в .NET Framework весьма просто. Для управления печатью предназначены три класса: PrintDialog, PrintPreviewDialog и PrintDocument.

Класс PrintDialog служит для вывода диалогового окна, предлагающего напечатать документ и позволяющего настроить параметры печати. Класс PrintPreviewDialog "печатает" документ на экране, благодаря чему пользователь может еще до печати увидеть, как будет выглядеть документ. Наконец, класс PrintDocument формирует выводимый документ и посылает его на печать.

Вывести диалоговое окно PrintDialog несложно: присвойте свойству Document ссылку на класс PrintDocument и вызовите метод ShowDialog. Класс PrintDialog не обеспечивает автоматическую печать документа; вы должны проверить DialogResult и вызвать метод Print объекта PrintDocument. Вывод PrintPreviewDialog осуществляется аналогично - с тем исключением, что DialogResult не проверяется. Если пользователь решает печатать документ из этого диалогового окна, вызывается метод Print класса PrintDocument.

Dim pDialogResult As DialogResult = PrintDialog1.ShowDialog()
If pDialogResult = DialogResult.OK Then PrintDocument1.Print()

Теперь, когда вы знаете, как реализовать печать, рассмотрим, как TaskVision формирует вывод на печать. Обычно для этого создается экземпляр класса PrintDocument, присваиваются значения свойствам, определяющим процесс печати, и вызывается метод Print, запускающий печать документа. В дальнейшем обрабатывается событие PrintPage, в котором определяется, что именно выводится на печать. Для формирования печатаемого изображения используется объект Graphics, содержащийся в параметре PrintPageEventArgs. В приложении TaskVision при обработке события PrintPage объект Graphics передается в написанный нами класс DataGridPrinter (название класса объясняется тем, что он демонстрирует, как печатается информация из DataGrid).

Класс DataGridPrinter выводит информацию в два этапа: сначала печатается заголовок страницы (имена столбцов), затем выводятся записи с данными.

При печати заголовка страницы (см. код ниже) мы с помощью объекта Graphics выводим прямоугольник, закрашенный серым цветом. Затем выполняем цикл по полям DataGrid, определяя показываемые в данный момент столбцы (с шириной > 0). Для каждого видимого столбца создается прямоугольник, определяющий, где печатается заголовок, а затем с помощью объекта Graphics в прямоугольнике печатается имя столбца. (Заметьте, что сами прямоугольники не рисуются, а только задают область вывода.)

Private Sub DrawPageHeader(ByVal g As Graphics)
   ´ Создаем прямоугольник заголовка
   Dim headerBounds As New RectangleF(c_LeftMargin, c_TopMargin, _
      m_PageWidthMinusMargins, m_DataGrid.HeaderFont.SizeInPoints + _
      c_VerticalCellLeeway)

   ´ Рисуем прямоугольник заголовка
   g.FillRectangle(New SolidBrush(m_DataGrid.HeaderBackColor), headerBounds)

   Dim xPosition As Single = c_LeftMargin + 12 ´ +12 for some padding

   ´ Используем этот формат при последующем рисовании
   Dim cellFormat As New StringFormat()
   cellFormat.Trimming = StringTrimming.Word
   cellFormat.FormatFlags = StringFormatFlags.NoWrap Or _
   StringFormatFlags.LineLimit

   ´ Берем имена столбцов из стиля таблицы (tablestyle)
   Dim cs As DataGridColumnStyle
   For Each cs In m_DataGrid.TableStyles(0).GridColumnStyles
      If cs.Width > 0 Then
         ´ Используем при рисовании временную переменную, содержащую ширину
         Dim columnWidth As Integer = cs.Width

         ´ Вычисляем ширину столбца с кратким описанием задачи так,
         ´ чтобы использовалась вся ширина страницы.
         ´ Примечание: это лишь простой способ добиться, чтобы текст занимал
         ´ всю ширину страницы; это не лучший вариант, но в данном случае
         ´ достаточно и такого способа
         If cs.MappingName = "TaskSummary" And m_IsTooWide Then
            columnWidth -= m_AdjColumnBy
         ElseIf cs.MappingName = "TaskSummary" Then
            columnWidth += m_AdjColumnBy
         End If

         ´ Создаем прямоугольник вывода
         Dim cellBounds As New RectangleF(xPosition, c_TopMargin, columnWidth, _
            m_DataGrid.HeaderFont.SizeInPoints + c_VerticalCellLeeway)

         ´ Выводим имя столбца
         g.DrawString(cs.HeaderText, m_DataGrid.HeaderFont, 
	New SolidBrush(m_DataGrid.HeaderForeColor), cellBounds, cellFormat)

         ´ Вычисляем следующую X-координату
         xPosition += columnWidth
      End If
   Next
End Sub

Вывод записей аналогичен выводу заголовка страницы за исключением того, что сначала выполняется цикл по каждому DataRow, входящему в состав DataTable; мы выполняем цикл по столбцам, чтобы определить, нужно ли показывать значение, содержащееся в текущей ячейке (width > 0). Затем проверяем имя поля, чтобы определить, требуется ли просто напечатать значение или вывести изображение, соответствующее значению.

Элементы управления Expander и Expander List

Класс Expander - элемент управления, используемый для размещения диаграмм Priority и Overall Progress и панели Task History. Класс ExpanderList является контейнером для объектов Expander. При добавлении элемента управления в контейнер ExpanderList выполняется проверка типа. Если добавляемый элемент управления имеет тип Expander, объект ExpanderList начинает обрабатывать события ControlCollapsed и ControlExpanded этого объекта Expander. При обработке событий программно вычисляются свойства Location всех элементов управления Expander. Таким образом, эти элементы управления перемещаются вверх или вниз, и обеспечивается их правильное размещение. Кроме того, элемент управления ExpanderList реализует поддержку на этапе разработки: при выполнении операции drag-and-drop автоматически центрирует и размещает элементы управления Expander.

    Public Sub ControlExpanded(ByVal x As XPander)
        Dim ctl As Control


        Dim enumerator As IDictionaryEnumerator = m_ControlList.GetEnumerator()
        While enumerator.MoveNext
            ctl = CType(enumerator.Value, Control)
            If ctl.Top > x.Top Then
                ctl.Top += x.ExpandedHeight - x.CaptionHeight
            End If
        End While
    End Sub

Классы ExpanderList и Expander - это подключаемые скомпилированные библиотеки. Тем не менее мы покажем, как с помощью GDI+ выполняется градиентная заливка заголовка элемента Expander. Примечание: конструктор LinearGradientBrush принимает начальный цвет (Color.White) и конечный цвет (переменная CaptionColor содержит Color.FromArgb(198, 210, 248)).

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
   Dim rc As New Rectangle(0, 0, Me.Width, CaptionHeight)
   Dim b As New LinearGradientBrush(rc, Color.White, CaptionColor, _
      LinearGradientMode.Horizontal)

   ´ Рисуем область заголовка вверху
   e.Graphics.FillRectangle(b, rc)

Элемент управления Custom Chart

Класс CustomChartControl включается как скомпилированная библиотека. Чтобы воспользоваться этим классом, присвойте значения членам DataTable и DataMember (имя поля). Элемент управления CustomChartControl рисует круговую диаграмму, показывающую процентное соотношение значений полей. В нашем случае эта диаграмма, выводимая через GDI+, используется для отображения процентного соотношения задач с тем или иным приоритетом, входящих в текущий проект.

chartPriority.DataTable = m_DataLayer.DsTasks.Tasks
chartPriority.DataMember = "PriorityText"

Элемент управления Progress Chart

Как и Custom Chart, элемент управления Progress Chart является подключаемой скомпилированной библиотекой. Он использует возможности GDI+ и выводит прямоугольную диаграмму, показывающую, на сколько процентов в среднем выполнен текущий проект.

chartProgress.DataTable = m_DataLayer.DsTasks.Tasks
chartProgress.DataMember = "Progress"

Элемент управления History Panel

Класс TaskHistoryPanel - простой элемент управления, который перебирает в DataView хронологию задач и автоматически добавляет элемент управления LinkLabel для каждой записи, соответствующей текущему TaskID.

Добавление элементов управления LinkLabel в History Panel приводит к тому, что в унаследованное свойство Control.Tag заносится целое значение, идентифицирующее запись, и устанавливается обработчик события щелчка на LinkLabel - функция, определенная в нашем коде.

´ Создаем элемент управления linklabel
Dim newLinkLabel As New LinkLabel()
newLinkLabel.Text = datePrefix & CType(row.Item(m_DisplayMember), String)
newLinkLabel.Location = New System.Drawing.Point(c_LinkLabelStartingX, _
   (c_LinkLabelStartingY + (numLinks * c_LinkLabelHeight)))
newLinkLabel.Size = New System.Drawing.Size((Me.Width - _
   (c_LinkLabelStartingX * 2)), c_LinkLabelHeight)
newLinkLabel.Name = "LinkLabel" & numLinks
newLinkLabel.Tag = (numLinks)
newLinkLabel.TabStop = True
newLinkLabel.FlatStyle = FlatStyle.System

´ Добавляем LinkLabel
Me.Controls.Add(newLinkLabel)
AddHandler newLinkLabel.Click, AddressOf LinkLabel_Click

´ Увеличиваем число ссылок на 1
numLinks += 1

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

    Private Sub LinkLabel_Click(ByVal sender As Object, ByVal e As _
        System.EventArgs)

        ´ Повторно генерируем событие Click, но передаем свои параметры
        Dim link As LinkLabel = CType(sender, LinkLabel)
        RaiseEvent HistoryLinkClicked(m_SelectedTaskID, _
            CType(link.Tag, Integer))
    End Sub

Класс DataProtection

Так как клиентское приложение хранит пароли в реестре, нужно обеспечить, чтобы никакой любопытный пользователь не мог подсмотреть пароли других пользователей. Эту задачу можно решить несколькими способами. Мы решили использовать функции реализованного в Windows 2000/XP DPAPI (Data Protection API) - CryptProtectData и CryptUnprotectData, - позволяющие защищать секретную информацию, не прибегая к прямому управлению ключами.

Класс DataProtection нашего проекта по сути является оболочкой функций DPAPI. Ниже показано, как сохранить зашифрованный пароль в реестре, а потом прочитать и расшифровать его.

Информацию о DPAPI см. в статье MSDN Windows Data Protection.

´ Присваиваем параметру реестра значение, содержащее зашифрованный текст
Dim regKey As RegistryKey = Registry.CurrentUser.CreateSubKey(c_RegistryKey)
regKey.SetValue("Password", _
   DataProtection.ProtectData(txtPassword.Text, "TaskVisionPassword"))

´ Присваиваем строке текст, полученный в результате расшифровки
´ значения параметра реестра
Dim password As String = String.Empty
password = DataProtection.UnprotectData(CType(regKey.GetValue("Password"), _
      String))

Поддерживаемые возможности

  • Локализация. Это создание локализованных версий ресурсов (для различных языков с учетом особенностей региональных стандартов вроде календаря), которые должны поддерживаться приложением. Основными средствами локализации приложений для .NET Framework являются диспетчер ресурсов (resource manager), файлы ресурсов и сопутствующие сборки (satellite assemblies). В Visual Studio .NET создание этих файлов и сборок упрощается благодаря использованию ряда свойств, настраиваемых в дизайнере Windows Forms. В TaskVision версии 1.1 выполнена локализация для немецкого языка. В этой версии имеется сопутствующая сборка, из которой все формы извлекают ресурсы и значения свойств. Для каждой формы поддерживается файл ресурсов, которые используются по умолчанию, а также содержат значения свойств и изображения, относящиеся к культуре по умолчанию. Кроме того, для каждой формы имеется файл "<имя формы>.de.resx", содержащий значения свойств и изображения для немецкой культуры (German). Эти файлы поддерживаются Visual Studio .NET и содержат только данные, специфичные для соответствующих форм. Кроме того, если требуется хранить нестандартные локализованные строки (например, сообщения, выводимые при генерации исключений), нужно создать дополнительные файлы ресурсов. В TaskVision поддерживаются два таких файла - "localize.resx" and "localize.de.resx", где хранятся определенные нами строки. Дополнительную информацию о файлах локализации см. в статье MSDN Introduction to Resources and Localization.
  • COM Interop. Взаимодействие с COM (COM Interop) - позволяет использовать существующие COM-объекты в приложениях для платформы .NET. В TaskVision применение COM Interop демонстрируется на примере обращения к Excel 10.0 Type Library и автоматизации MS Excel для создания электронной таблицы и ее заполнения данными TaskVision. При работе с COM Interop следует учесть два момента. Во-первых, чтобы получить возможность обращаться к COM-объекту, необходимо установить на компьютер разработчика соответствующее программное обеспечение (в нашем примере Excel). Во-вторых, разработчики должны принимать во внимание ситуацию, когда это программное обеспечение (или COM-объекты) не установлено на компьютере пользователя. Дополнительную информацию о COM Interop см. в статье MSDN Introduction to COM Interop.
  • Специальные возможности (accessibility). Чтобы показать одну из основных специальных возможностей, поддерживаемых Visual Studio .NET, мы не пожалели сил и задали значения свойств AccessibleDescription и AccessibleName всех элементов управления. Эти свойства играют важную роль в приложениях с поддержкой специальных возможностей, таких как Microsoft Narrator, который в период выполнения обращается к элементам управления и по буквам зачитывает пользователю описания этих элементов управления.
  • Динамические свойства. Позволяют конфигурировать приложение так, чтобы некоторые или все свойства хранились во внешнем файле конфигурации, а не в скомпилированном коде. Предоставляя динамические свойства, вы даете возможность администраторам обновлять их значения, которые могут изменяться со временем. Это сокращает издержки на сопровождение приложения после его развертывания. Например, вы пишете приложение, которое на этапе разработки использует тестовую базу данных, а при развертывании приложения нужно переключиться на настоящую базу данных, используемую предприятием. Если вы храните значения свойств внутри приложения, то перед развертыванием придется вручную изменить все параметры, описывающие базу данных, и перекомпилировать исходный код. При хранении этих параметров вне приложения достаточно один раз изменить внешний XML-файл - тогда при следующем запуске приложение будет работать с новыми значениями. В TaskVision примером использования динамических свойств является хранение URL компонента обновления (Updater Component) и Web-сервисов Authentication и Data в файле TaskVision.exe.config.
  • Темы Windows XP. В Windows XP используется новая версия библиотеки Shell Common Controls (COMCTL32.DLL 6.0). В этой библиотеке появились новые версии элементов управления (например, кнопок и вкладок), в которых используются насыщенные цвета и скругленные края (см. иллюстрации ниже). Чтобы задействовать новую версию стандартных элементов управления, приложение должно явно запросить ее, предоставив свой манифест. Манифест приложения передается Windows XP как отдельный файл или ресурс, присоединенный к исполняемому файлу. Если манифест содержится в отдельном файле, этот файл должен находиться в том же каталоге, что и исполняемый файл. Именем файла манифеста должно быть имя исполняемого файла, к которому добавлено расширение .manifest. Так, файл манифеста для TaskVision.exe - TaskVision.exe.manifest. Кроме указания манифеста, для поддержки тем Windows XP многим элементам управления необходимо, чтобы свойство FlatStyle имело значение System.

    Рис. 18. Окно приложения, не использующего файл манифеста

    Рис. 19. Окно приложения, использующего файл манифеста

Усвоенные уроки

TaskVision - это пример решения, демонстрирующий, какие мощные возможности предоставляют интеллектуальные клиентские приложения, разрабатываемые в .NET Framework.

Как и многие другие проекты, решение TaskVision развивалось, и на этапе разработки в него вносились изменения. Если бы мы заново начали работать над проектом, то поступили бы по-другому в следующих двух случаях.

Компонент доступа к данным

При более глубоком знакомстве с клиентским приложением вы заметите в UI небольшие побочные эффекты, связанные с архитектурой данных приложения. Элементы управления Windows Forms поддерживают концепцию связывания с данными, благодаря чему элемент управления автоматически заполняется значениями, полученными из источника данных (или отражает изменения этих значений). Поскольку мы используем Web-сервисы XML, при передаче объекта сервису и изменении объекта связанные с ним элементы управления не обновляются автоматически. Вместо этого приходится выбирать один из двух вариантов: объединять возвращаемый обновленный DataSet с текущим DataSet после каждого вызова Web-сервиса XML или обновлять связи элементов управления с данными после каждого вызова Web-сервиса XML. Мы выбрали слияние обновленного и существующего DataSet, что позволило поддерживать концепцию связывания с данными. Но это-то и вызывает побочные эффекты - например, после обновления текущая запись DataGrid теряет фокус. Если бы была возможность начать все заново, мы выбрали бы обновление связей с данными после каждого вызова Web-сервиса XML, выполняющего обновление данных, так как тогда удалось бы избежать нежелательных побочных эффектов.

Безопасность Web-сервисов XML

Заслуживает упоминания и Web Services Enhancements (WSE) 1.0 for Microsoft .NET - набор инструментов, позволяющий разработчикам для Visual Studio .NET и .NET Framework поддерживать новейшие спецификации Web-сервисов XML, в частности WS-Security, WS-Routing, WS-Attachments и DIME. К сожалению, WSE был недоступен в то время, когда мы разрабатывали решение. Поэтому мы не показали, как с помощью определенных компонентов WSE, например поддержки WS-Security, обеспечить более гибкую и управляемую защиту Web-сервисов XML. Мы собираемся продемонстрировать эти возможности в другом полномасштабном примере приложения, который планируем выпустить в этом году. Более подробную информацию о WSE см. на странице MSDN WSE home page.

Дополнительные ссылки

Complete TaskVision documentation and source code
TaskVision Source Code Viewer
TaskVision Discussion Forum
Web Services Enhancements 1.0 for Microsoft .NET
Localization and Globalization information
.NET Developer Center
.NET Framework product site
Visual Studio .NET product site


Может пригодится:


Автор: Vertigo Software, Inc
Прочитано: 3870
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру