Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум на Исходниках.RU > .NET FAQ > Разнопоточный доступ к GUI


Автор: PIL 03.10.06, 10:36
По следам недавних вопросов на форуме, и в связи с неосвещенностью вопроса в FAQ родилась эта небольшая статья:
Создан тестовый проект (VS2005) со следующим функционалом:
- при нажатии на кнопку Start создается отдельный поток, который в цикле записывает данные в базу данных (в данном примере использовался Access).
- после каждого добавления записи, при успешности операции - двигается прогресс бар для индикации процесса заливки записей.
- после завершения заливки данных разблокируется кнопка Start для возможности повторить еще раз процесс заливки данных.
Код обработчика кнопки старта процесса:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    private void button1_Click(object sender, EventArgs e)
            {
                //Control.CheckForIllegalCrossThreadCalls = false;
                button1.Enabled = false;
                Thread th = new Thread(new ParameterizedThreadStart(DoGenerateRecords));
                th.Start(connectionString);
            }
Я специально использовал ParameterizedThreadStart для того, чтобы показать, как создаются потоки с передачей параметров.
Код рабочего метода потока:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    /// <summary>
            /// process generate records to DB
            /// </summary>
            private void DoGenerateRecords(object param)
            {
                OleDbConnection conn = new OleDbConnection((string)param);
                OleDbCommand cmd = new OleDbCommand("ADD_UNIT", conn);
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.Parameters.Add("@name", OleDbType.Char, 50);
                cmd.Parameters.Add("@note", OleDbType.Char, 50);
                int realCount = 0;
                try
                {
                    conn.Open();
                    for (int i = 0; i < COUNT; i++)
                    {
                        cmd.Parameters["@name"].Value = "some name " + i.ToString();
                        cmd.Parameters["@note"].Value = "some note " + i.ToString();
                        if (cmd.ExecuteNonQuery() > 0)
                        {
                            realCount++;
                            //не правильный доступ.
                            //progressBar1.Value ++;
     
                            //правильный доступ
                            if (onProgress != null) onProgress(realCount);
                        }
                    }
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
                finally
                {
                    if (conn.State == ConnectionState.Open) conn.Close();
                    if (onEndProcess != null) onEndProcess();
                }
            }
Если раскоментировать строку progressBar1.Value ++; в рабочем методе потока, приложение заработает, проблем не будет, но: если только попробовать запустить Debug, мы получим эксепшн:
Cross thread operation not valid:....., это среда разработки предупреждает о том, что возможны ошибки при доступе к GUI из другого потока. Для ленивых программистов - эту генерацию исключений можно отключить -
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    Control.CheckForIllegalCrossThreadCalls = false;

, но как говорил Винни-Пух, "Нужно делать так как нужно, а так как не нужно делать не нужно" :)
Дальше:
Для добавления записей использовался хранимый запрос ADD_UNIT (синтаксис можно посмотреть в аттаче).
Для сигнализации окончания процесса и прогресса использовались делегаты:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    delegate void AddProgressEventHandler(int val);
    delegate void EndPocessEventHandler();
    //
     private event AddProgressEventHandler onProgress;
     private event EndPocessEventHandler onEndProcess;

И собственно сам код подписчиков событий:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    void Form1_onEndProcess()
            {
                if (button1.InvokeRequired)
                {
                    this.BeginInvoke(
                        new EndPocessEventHandler(Form1_onEndProcess));
                }
                else button1.Enabled = true;
            }
     
            void Form1_onProgress(int val)
            {
                if (progressBar1.InvokeRequired)
                {
                    this.BeginInvoke(
                        new AddProgressEventHandler(Form1_onProgress),
                        new object[] { val });
                }
                else progressBar1.Value = val;
            }

свойство InvokeRequired элемента управления возвращает true, если достут к элементу управления осуществляется с другого потока. Методы Invoke/BeginInvoke формы позволяют выполнить метод в потоке GUI формы. При этом Invoke - синхронно, BeginInvoke - ассинхронно.
Все, разрулили, теперь и студия не бросается исключениями, и на сердце спокойней :)
Спасибо тем, кто дочитал до конца, в аттаче - рабочий проект (VS2005). Жду критики и коментариев.

Автор: PIL 10.10.06, 13:20
Пришло письмо от робота, что архив битый, проверил, все нормально, но на всякий случай выкладываю еще раз:

Автор: ANDLL 11.09.07, 16:53
Некорректность(нельзя не отметить, очень типичная): После BeginInvoke должен идти EndInvoke, обязательно. Архивы(оба) действительно битые, по крайней мере в explorer(vista) не открываются

Автор: GazOn 20.09.07, 06:39
Если архив распаковать WinRar'ом, то там лежит один файл с неизвестным расширением.

Автор: hinews 30.11.07, 17:46
Просто внутри архива лежит еще один архив. Так добавьте к этому файлу расширение .zip и будет тот самый архив.

Автор: PIL 11.03.08, 09:52
Цитата ANDLL @
После BeginInvoke должен идти EndInvoke, обязательно.
- Нет, не обязательно, читаем Дж. Рихтера CLR via С# 2005 - стр. 604, - для GUI - это единственное исключение из правил, когда EndInvoke не обязателен

Автор: Alexus 21.03.08, 10:25
Предвосхищая аналогичные вопросы по WPF - там Invoke можно найти в объекте типа System.Windows.Threading.Dispatcher. Ссылку на объект этого типа держит само приложение (System.Windows.Application.Current.Dispatcher), а так же все объекты, наследованные от System.Windows.Threading.DispatcherObject (это все контролы, окна, и даже все объект, кторые имеют зависимыс свойства(DependencyProperties)).

Автор: mr.Torrens 29.01.10, 11:58
А если используется несколько потоков и из них необходимо править разные свойства десятка объектов(button, progress bar, listview), то для каждого надо прописывать такие события? Нет какого-то универсального решения?

Автор: juice 31.01.10, 19:40
Асинхронное программирование может чем то поможет.

Автор: PIL 01.02.10, 08:09
Цитата mr.Torrens @
А если используется несколько потоков и из них необходимо править разные свойства десятка объектов(button, progress bar, listview), то для каждого надо прописывать такие события? Нет какого-то универсального решения?
- BackgroundWorker, SyncronizationContext. У меня на блоге немного инфы есть

Автор: Raistlin 06.02.10, 19:17
Только что искал информацию по данной тематике для WPF. На первых строчках поисковика данная статья и еще вот эта:
http://msdn.microsoft.com/ru-ru/magazine/cc163328.aspx?ppud=4
Нашел все, что было нужно :rolleyes:

Powered by Invision Power Board (https://www.invisionboard.com)
© Invision Power Services (https://www.invisionpower.com)