Я думаю вы сталкивались с ситуацией, когда нужно что-то импортить, например CSV файл с продуктами, кастомерами, синхронизировать базы или работать с каким то тормознутым веб сервисом. Во всех перечисленных и подобных случаях страница "лочится" пока не закончится операция, но нам хотелось бы видеть какой-то прогресс, о чем сегодня и поговорим.
Недавно мне нужно было импортить продукты из индусского сервиса, не важно как, главное что страница лочилась на 1-2 минуты и непонятно, что там происходило и когда оно завершалось.
Существует несколько решений как можно узнать прогресс на длительной операции:
- Сохранять прогресс в сессию, а на клиенте должен быть таймер, который после запуска длительной операции будет опрашивать страницу с какой-то периодичностью, например каждую секунду или тп. Этот метод описан здесь;
- Создать вспомогательную страницу в iframe, при запуске которой в OnLoad сразу начинается длительная операция. Во время прогресса надо писать в Response вызов JS функции, которая будет вызываться в родительском окне, с которого был запущен iframe. Этот метод описан здесь;
- Сделать страницу асинхронной, через Async или ICallbackEventHandler Interface и опять-таки через таймер опрашивать сервер. Это способ можно отнести как вариация второго.
Из всех перечисленных мне не нравится никакой :)
- Потому, что надо делать вспомогательную страницу. Хоть автор и советует использовать только этот метод, но мне он кажется недоделаным.
- Потому, что както-то по индусски... опрашивать постоянно сервер? может в каком-то случае это и будет лучший вариант, но не сегодня.
- Ограниченные возможности по передаче параметров от клиента к серверу и наоборот. Можно передать, что угодно, но только в одной строке. А дальше думать формат, парсить... ну и + оргументы выше в пункте 2.
Мое решение похожее больше на 1 вариант, но без iframe. В результате получим примерно следующее:
На картинках выше показана индикация прогресса во время импорта продуктов, категорий и брендов из сервиса. Initialization показывает прогресс когда выполняется аутентификация или может создание вспомогательный классов, по вашему желанию. В мое случае я логинился на сервис. А теперь как это делалось. Для этого нам понадобится
одна! страница, класс с длительной операцией, интерфейс через который будет уведомляться страница о прогрессе, пару js функций которые будут двигать прогресс бар и немного css для прогресс бара.
public interface IProgressReporter
{
void ReportInitializationFinished();
void ReportCategoriesProgress(int count, int completed);
void ReportProductsProgress(int count, int completed);
void ReportBrandsProgress(int count, int completed);
void ReportException(Exception exception);
}
Методы интерфейса как я и говорил мы будет дергать из класса, который занимает длительным процессом. Ниже приведен сам класс, он примитивный, просто чтоб показать саму суть.
public class LongOpperations
{
public void ProcessCatalog(IProgressReporter progressReporter)
{
// some initialization, web service authentication, whatever
Thread.Sleep(5000);
progressReporter.ReportInitializationFinished();
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
progressReporter.ReportCategoriesProgress(100, i + 1);
}
for (int i = 0; i < 20; i++)
{
Thread.Sleep(200);
progressReporter.ReportBrandsProgress(20, i + 1);
}
for (int i = 0; i < 100; i++)
{
Thread.Sleep(200);
progressReporter.ReportProductsProgress(100, i + 1);
}
}
}
Отчетом о прогрессе выступает наша страница, для этого нужно реализовать наш интерфейс. Потом, когда вы будем стартовать длительную процедуру, мы передадим себя, страницу, в наш обработчик. В итоге получаем подобие шаблона Visitor. Самое интересное находится в методе Render. Начало ясное, если мы не нажали кнопку, то просто рендерим страницу как есть. Дальше мы должны отключить буферизацию страницы, в другом случае IE например закеширует первые 256 байт ответа и мы получим не то, что ожидали. Поэтому отключаем. Если мы после этого начнем репортить прогресс, то мы перетрем то, что существует на странице, поэтому ренедрим её как есть, а потом будет дописывать в Response наши ответы. Основная идея заключается в том, чтоб в Response записать вызов клиентской функции и сразу вывести его на страницу через Flush. Как только на странице появится script, он сразу выполнится, тем самым подвинет прогресс бары.
public partial class _Default : Page, IProgressReporter
{
private bool started;
protected override void Render(HtmlTextWriter writer)
{
if (!started)
{
base.Render(writer);
return;
}
Response.BufferOutput = false;
base.Render(writer);
Response.Flush();
ReportInitializationStarted();
new LongOpperations().ProcessCatalog(this);
}
protected void OnStartClick(object sender, EventArgs e)
{
started = true;
}
public void ReportInitializationStarted()
{
Response.Write("<script>ReportInitializationStarted();</script>");
Response.Flush();
}
public void ReportInitializationFinished()
{
Response.Write("<script>ReportInitializationFinished();</script>");
Response.Flush();
}
public void ReportCategoriesProgress(int count, int completed)
{
ReportProgress(pnlCategoryProgress.ClientID, lblCategory.ClientID, count, completed);
}
public void ReportProductsProgress(int count, int completed)
{
ReportProgress(pnlProductProgress.ClientID, lblProduct.ClientID, count, completed);
}
public void ReportBrandsProgress(int count, int completed)
{
ReportProgress(pnlBrandProgress.ClientID, lblBrand.ClientID, count, completed);
}
public void ReportException(Exception exception)
{
string progressCall = string.Format("<script>ReportException('{0}','{1}');</script>", exception.Message, exception.StackTrace);
Response.Write(progressCall);
Response.Flush();
}
private void ReportProgress(string progressBar, string percentLabel, int count, int completed)
{
double progress = (count == 0) ? 100 : (100.0 / count) * completed;
string progressCall = string.Format("<script>UpdateProgress('{0}','{1}',{2},{3},{4});</script>", progressBar, percentLabel, progress, count, completed);
Response.Write(progressCall);
Response.Flush();
}
Клиентский функции, которые двигают прогресс бары и тп.
function UpdateProgress(progressBarId, percentLabelId, progress, count, completed) {
var progressBar = document.getElementById(progressBarId);
var progressLabel = document.getElementById(percentLabelId);
progressLabel.innerHTML = (progress == -1) ? '0' : completed + '/' + count;
progressBar.style.width = progress + '%';
};
function ReportException(message, stack) {
var p = document.createElement('p');
p.innerHTML = 'Message: ' + message + '
Stack: ' + stack;
document.getElementById('error').appendChild(p);
};
function ReportInitializationFinished() {
var pnl = document.getElementById('pnlInitializing');
pnl.className = null;
};
function ReportInitializationStarted() {
var pnl = document.getElementById('pnlInitializing');
pnl.className = 'progress';
pnl.style.width = '100%'
};
Вот еще кусок разметки для ясности. Весь приводить не буду, т.к. там тоже самое и немного css.
Вот и все. Основная идея в том, чтоб запустив длительный процесс сразу отдавать ответ в виде js сриптовой инъекции, которая будет обновлять прогресс. Вы можете наделать любых методов в интерфейсе. Они могут быть настолько детальными на сколько вам необходимо. В моем случае я хотел еще справа от прогресс баров добавить лог ошибок и добавил соответсвующий метод в инерфейсе ReportException, оставил для примера.
Буду рад услышать ваши идеи и отзывы.