Постановка задачи
Индикаторы выполнения хорошо подходят для считывания данных из потока. Потоки
данных хороши потому, что вы можете считать из них настолько мало или настолько
много, насколько хотите, и каждое считывание будет завершаться отдельно. Т.е.
если вам надо считать из потока данных 200 байт, вы можете разбить это
количество на десять 20-байтовых считываний и затем приращивать индикатор
выполнения в соответствие с каждым считыванием.
Если вы хорошо знакомы с отправлением HTTP запросов и считыванием HTTP
ответов с помощью методов HttpWebRequest.GetRequestStream и
HttpWebResponse.GetResponseStream, тогда вы знаете возможности
потоковой передачи данных запроса и ответа в базовой поддержке, используемой Web
методами. Например, базовые TCP соединения, которые переносятся полезным грузом
HTTP и SOAP, являются потоковыми соединениями.
Проблема в том, что, если весь механизм предоставляется через простой вызов
метода объекта, намного проще отправить SOAP сообщение и прочитать ответ. Эту
простоту использования .NET Framework обеспечивает путем создания прокси класса,
который предоставляет отдельные методы вашему приложению. Перед отправкой по
проводам поддержка Web сервиса сериализует параметры в SOAP запрос. Аналогичным
образом, ответ десериализуется из SOAP потока в информацию ответа на вызов
метода. Нет смысла просто сериализовать и десериализовать порцию данных.
(Означает ли это, что некоторые параметры существуют, а другие нет?)
Решение задачи
Выход в том, чтобы внедриться в запрос и ответ на нижнем уровне. Со стороны
сервера Microsoft® ASP.NET предоставляет изобилие интерфейсов для проникновения
в механизм обработки запроса. Со стороны клиента, однако, мы ограничены. К
счастью, встроенная поддержка для отправки и получения SOAP запросов для вызовов
Web методов предлагает эквивалентные поддержке, предоставляемой на сервере для
внедрения в SOAP запросы и ответы, функциональные возможности. Правильно,
решение – использовать SOAP расширение.
Написание SOAP расширения на клиенте аналогично написанию его на сервере.
Единственным отличием является порядок действий. На сервере получаемый запрос
десериализуется и затем перед отправкой обратно к клиенту сериализуется ответ.
На клиенте запрос сначала должен быть сериализован для отправки, а затем
полученный ответ должен быть десериализован. В нашем конкретном случае мы хотим
отлавливать запрос так, как будто он был возвращен, как можно более близко к
проводам.
Расширение SOAP – это просто класс, наследуемый от класса
System.Web.Services.Protocols.SoapExtension. Есть некоторое
количество методов инициализации, которые вы должны переопределить в вашем
классе, но, в отличие от переопределения по умолчанию, для наших целей
практически все они могут быть проигнорированы. В основном, мы рассматриваем
только два переопределяемых метода класса SoapExtension:
ChainStream и ProcessMessage.
ChainStream – это механизм, используемый для добавления
вашего собственного потока в "цепь" потоков. Это обеспечивает вашему SOAP
расширению возможность видеть и потенциально изменять входящие и исходящие
данные. ChainStream вызывается дважды за запрос: первый раз -
во время создания потока запроса, и второй раз - во время создания потока
ответа. Для обновления индикатора выполнения при получении ответного сообщения,
мы, в частности, заинтересованы в проникновении в поток ответа.
Переопределение ProcessMessage в вашем SOAP расширении будет
вызываться на четырех этапах в течение обработки одного SOAP запроса и ответа.
Стадии BeforeSerialize и AfterSerialize имеют
место при сериализации запроса, стадии BeforeDeserialize и
AfterDeserialize – при десериализации ответа. Нас интересует
ответ. Мы хотим отслеживать данные по мере их прохождения по сети, а не после
того как они уже буферизованы и десериализованы. Поэтому основную часть нашей
работы мы будем делать на этапе BeforeDeserialize.
Одним из необычных требований нашего SOAP расширения является то, что ему
понадобится взаимодействовать с пользовательским интерфейсом нашего приложения.
В частности, ему понадобится обновлять индикатор выполнения, что должно
происходить в определенном потоке, который обладает описателем базового окна
элемента управления. В обычных многопоточных Microsoft® Windows приложениях,
чтобы запустить делегат в потоке элемента управления, вы просто используете
функцию-делегат и метод Invoke элемента управления, и они
выполняют работу по взаимодействию с элементом управления. В нашем случае SOAP
расширение реализовывается в классе, отдельном от пользовательского интерфейса,
поэтому нам нужен способ передать ссылку на конкретный экземпляр элемента
управления в класс нашего SOAP расширения.
Чтобы сделать это, я создал специальный класс, унаследованный от прокси
класса Web сервиса, который был создан через Add Web Reference возможность
Microsoft® Visual Studio® .NET. Вы можете получить экземпляр прокси класса,
ассоциированного с запросом, сначала преобразовывая переданный в функцию
ProcessMessage параметр SoapMessage в
SoapClientMessage, и затем обращаясь к свойству Client. Я
просто добавляю в созданный мною класс (унаследованный от исходного прокси
класса Web сервиса) public член, который сохраняет любую необходимую мне
информацию. В данном случае, в качестве public члена я добавил ссылку на объект
индикатора обработки. Тот же механизм я использую для передачи из приложения
такой информации, как размер ожидаемых мною данных и функция-делегат, которую
необходимо использовать для связи с индикатором выполнения. Далее приведен код
моего специального прокси класса:
internal class ProgressClient : localhost.Service1
{
public ProgressBar Progress;
public int TransferSize;
public Form1.UpdateDelegate ProgressDelegate;
}
Вот код моего класса SoapExtension:
public class ProgressExtension : SoapExtension
{
// Holds the original stream
private Stream m_oldStream;
// The new stream
private Stream m_newStream;
// The buffer for reading from the old stream
// and writing to the new stream
private byte[] m_bufferIn;
// The progress bar we will be incrementing
private ProgressBar m_Progress;
// The size of each read
private int m_readSize;
// The delegate we will invoke for updating the
// progress bar.
private Form1.UpdateDelegate m_progressDelegate;
// Used to keep track of which stream we are trying
// to chain into
private bool m_isAfterSerialization;
public override void ProcessMessage(SoapMessage message)
{
switch(message.Stage)
{
case SoapMessageStage.AfterSerialize:
// To let us know that the next ChainStream call
// will let us hook in where we want.
m_isAfterSerialization = true;
break;
case SoapMessageStage.BeforeDeserialize:
// This is where we stream through the data
SoapClientMessage clientMessage
= (SoapClientMessage)message;
if (clientMessage.Client is ProgressClient)
{
ProgressClient proxy
= (ProgressClient)clientMessage.Client;
m_Progress = proxy.Progress;
// Read 1/100th of the request at a time.
// This will give the progress bar 100
// notifications.
m_readSize = proxy.TransferSize / 100;
m_progressDelegate = proxy.ProgressDelegate;
}
while (true)
{
try
{
int bytesRead
= m_oldStream.Read(m_bufferIn,
0,
m_readSize);
if (bytesRead == 0)
{
// end of message...rewind the
// memory stream so it is ready
// to be read during deserial.
m_newStream.Seek(0,
System.IO.SeekOrigin.Begin);
return;
}
m_newStream.Write(m_bufferIn,
0,
bytesRead);
// Update the progress bar
m_Progress.Invoke(m_progressDelegate);
}
catch
{
// rewind the memory stream
m_newStream.Seek(0,
System.IO.SeekOrigin.Begin);
return;
}
}
}
}
public override Stream ChainStream(Stream stream)
{
if (m_isAfterSerialization)
{
m_oldStream = stream;
m_newStream = new MemoryStream();
m_bufferIn = new Byte[8192];
return m_newStream;
}
return stream;
}
// We don't have an initializer to be shared across streams
public override object GetInitializer(Type serviceType)
{
return null;
}
public override object GetInitializer(
LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute)
{
return null;
}
public override void Initialize(object initializer)
{m_isAfterSerialization = false;}
}
Чтобы SOAP расширение вызывалось для клиентского приложения, класс
SoapExtension должен быть соответственно конфигурирован. Для Microsoft® Windows®
Form приложения требуется модификация файла конфигурации. Для моего приложения
WebServiceProgress.exe файл конфигурации WebServiceProgress.exe.config выглядит
следующим образом:
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add
type="WebServiceProgress.ProgressExtension, WebServiceProgress"
priority="1" group="0" />
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
В атрибуте type элемента add показан тип
моего класса SoapExtension и сборки, в которой он находится.
Значение атрибута priority установлено равным 1, а значение атрибута group – 0,
для того чтобы SoapExtension имел наивысший уровень приоритета.
Мы хотим, чтобы считываемый нами поток был как можно более близок к «проводам»,
поэтому важно, чтобы между потоком SoapExtension и сетью не
было других потоков. Следовательно, мы устанавливаем максимальный приоритет. Но
все еще остается вероятность того, что в цепочку перед нашим расширением может
быть вставлено SOAP расширение. Если это произойдет, тогда поток, из которого мы
читаем, будет состоять только из буферизованных данных, которые не создадут нам
точной картины о пропускной способности нашей сети.
Последний участок кода, который нужен для всего этого – функция
delegate в главном классе для моего приложения, связанного с
формой. Это функция, которая и будет обновлять элемент управления индикатор
выполнения. Она должны быть объявлена как делегат и вызвана с помощью метода
Invoke, чтобы могла запуститься в том же потоке, где находится
подкачка сообщений окна, в котором живет элемент управления. Сначала вы должны
объявить тип делегата:
public delegate void UpdateDelegate();
Затем вы должны создать функцию с той же сигнатурой, которая обычно
осуществляет взаимодействие. В моем случае, свойство Maximum
индикатора выполнения установлен во время разработки на 100. в классе моего SOAP
расширения я считываю 1/100 ожидаемых данных в единицу времени. По завершении
каждого считывания, индикатор выполнения может увеличиться на 1, таким образом,
он достигнет своего максимума с окончанием последнего считывания. Код для
функции обновления моего индикатора выполнения просто выглядит следующим
образом:
private void ProgressBarUpdate()
{
progressBar1.Increment(1);
}
Другой потенциальной проблемой является то, что многие инструментальные
средства Web сервисов буферизуют ответ на сервере до тех пор, пока не
завершиться код Web сервиса. Это, конечно, происходит в том случае, если вы
имеете дело с сервером, написанным с использованием ASP.NET Web методами. Если
вы надеетесь обеспечить функциональность индикатора выполнения, который
отслеживает в коде Web сервиса процесс генерирования элементов большого массива,
тогда забудьте об этом. Если, однако, вы хотите, чтобы ваш индикатор выполнения
отслеживал длительный процесс передачи исключительно большого массива по сети ,
тогда вам повезло. Это решения как нельзя лучше подойдет для удовлетворения
ваших требований.
Помехой, общим для многих сценариев индикатора выполнения, является
неосведомленность о размере передаваемых данных. В моем примере приложение,
чтобы узнать это, использует входные данные пользователя, но существует огромное
количество сценариев, в которых приложение не будет знать размера данных.
Например, ваш ответ может ориентироваться на размер результирующего множества
запроса базы данных, но вы можете не иметь никакого представления о том, сколько
записей может быть возвращено. В таких сценариях вам просто надо приблизительно
подсчитать ожидаемый размер и решить, как работать с расхождениями во время
обновления индикатора выполнения.