При переделке старой формы столкнулся с забавной проблемой.
Задача – классическая: вывести пользователю информацию о происходящем в фоне процессе.
Казалось бы, ничего сложного. В основной форме мы стартуем поток, в нем проводим обработку данных, при получении новых статусов – сбрасываем обновление на форму, проводя синхронизацию с базовым UI Thread (Invoke / BeginInvoke call).
И все хорошо до момента, пока наш background поток не попытается создать еще один-два-… Которым делегирует дополнительную работу в рамках задачи. Вот с этими-то новыми потоками и начинается чехарда…
Итак, первый поток получил текст для обновления, этот текст будем выводить на форму.
private delegate void EditStatusTextDelegate(string strArg);
private void SendNotificationToForm( string actionText )
{
if (this.InvokeRequired)
{
this.Invoke(new EditStatusTextDelegate(UpdateUI), new object[] { actionText });
return;
}
UpdateUI(actionText);
}
private void UpdateUI(actionText)
{
this.LabelInfo.Text = actionText;
}
или
private void SendNotificationToForm( string actionText )
{
this.BeginInvoke((Action)(() =>
{
this.LabelInfo.Text = actionText;
}));
}
В рамках одного потока это работает – сообщения приходят, форма их получает, данные обновляются. При добавлении же еще нескольких, наша функция SendNotificationToForm начинает вести не совсем так, как ожидалось. Сообщения приходят и форма их получат, вот только складывает их в очередь для обновления контента и пока ваши background потоки не закончат работу, не спешит выводить на экран. И чем более сложной задачей заняты вы в других потоках, тем нагляднее это проявляется. Причем в реальном коде вам наверняка надо будет не только один Label изменить, вполне возможно, что набор данных для смены визуализации будет куда как более сложным.
Официальная документация на эту тему упорно говорит про InfokeRequired & BeginInvoke. Но на самом деле, в нашем случае придется отказаться от «обычного метода» и перейти к ручному управлению контекстом синхронизации. Так как нам нужно, чтобы обновления произошли в момент прихода данных на форму, то и придется провести «ручную синхронизацию».
Для этого – после создания формы (когда контекст уже создан и проинициализирован) – запомнить его:
Fields:
private readonly SynchronizationContext syncContext;
Constructor:
syncContext = SynchronizationContext.Current;
И теперь уже в нашем callback из другого потока выполним команду:
private void SendNotificationToForm( string actionText )
{
syncContext.Post( UpdateUI, actionText);
}
private void UpdateUI(actionText)
{
this.LabelInfo.Text = actionText;
}
В этом случае – мы принудительно провели синхронизацию данных для формы и заставили основной UI Thread обработать полученную информацию.
С точки зрения проблемы – решение простейшее, но эффективное. Но оно дает гарантию, что теперь пользователь увидит на форме всю информацию от работающих потоков, а не только часть данных от первого.
Автор: mynameiszb