Домашняя страница Проблема событий при редактировании компонентов из кода в WinForms
Публикация
Отменить

Проблема событий при редактировании компонентов из кода в WinForms

При работе с интерфейсом WinForms в C# часто появляется проблема с событиями компонентов. Проблема заключается в следующем: Если редактировать содержимое компонента из кода (например указать текущий выбранных элемент в ComboBox), то приходит событие об изменении содержимого, от этого компонента. Событие конечно правильно приходит, вот только реагировать на него не нужно и иногда даже вредно. Так же подписка на события может привести к рекурсивному вызову функции, который переполняет стек. Один из вариантов решения проблемы я представляю в этой статье.

Пример проблемы

Этот пример один из многих. Код написан так, чтобы было понятно суть проблемы. Допустим на форму добавлено несколько компонентов NumericUpDown, через которые идет редактирование полей некоторого класса. Так выглядит код инициализации компонентов:

1
2
3
4
5
6
private void    Init()
{
    numericCtrl1.Value = someClass.Valu1;
    numericCtrl2.Value = someClass.Valu2;
    numericCtrl3.Value = someClass.Valu3;
}

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

1
2
3
4
5
6
7
private void    OnInfoChanged(object sender, EventArgs e)
{
    someClass.Valu1 = numericCtrl1.Value;
    someClass.Valu2 = numericCtrl2.Value;
    someClass.Valu3 = numericCtrl3.Value;
    someClass.Regenerate();
}

Код логически верен. Проблема в том, что в функции Init трижды инициирует событие ValueChanged и будет вызываться функция OnInfoChanged. Это во-первых, перетрет содержимое класса someClass, и приведет его в невалидное состояние, а во-вторых, трижды вызовется функция Regenerate, которая может быть весьма ресурсоемкой. В данной ситуации нужно как-то проигнорировать событие.

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

Есть еще решение - отписываться от обработчиков событий на время выполнения кода. Этот вариант так же плох тем, что затрудняет читаемость кода, нужно писать комментарии для всего этого. Кроме того легко забыть или не верно зарефакторить обратную подписку на событие. Например добавить return в функцию, перед подпиской. Такие ошибки очень сложно заметить и искать.

Более универсальное решение, убирающее потенциальные ошибки, смотри под катом.

Решение

Для решения этой задачи разработан класс InterfaceLock, который блокирует вызовы обновлений интерфейса. Выглядит это следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void    Init()
{
    // блокировать другие вызовы, пока выполняется код
    InterfaceLock.Lock( this, delegate()
    {
        numericCtrl1.Value = someClass.Valu1;
        numericCtrl2.Value = someClass.Valu2;
        numericCtrl3.Value = someClass.Valu3;
    } );
}
private void    OnInfoChanged(object sender, EventArgs e)
{
    // блокировать другие вызовы, пока выполняется код
    InterfaceLock.Lock( this, delegate()
    {
        someClass.Valu1 = numericCtrl1.Value;
        someClass.Valu2 = numericCtrl2.Value;
        someClass.Valu3 = numericCtrl3.Value;
        someClass.Regenerate();
    } );
}

Вызывается функция InterfaceLock.Lock, которая блокирует определенный объект. Пока объект заблокирован, другие вызовы кода будут игнорироваться. Таким образом одновременно может выполняться только один блок кода для одного объекта. Причем это ни как не связано с потоками, не блокирует их, и может использоваться в многопоточной среде. Разумеется нет нужды обрамлять таким блоком весь код, только тот код, исполнение которого не желательно, пока выполняется другой код.

Исходный код класса InterfaceLock

Учитывая, что подобное блокирование будет идти стеком в однопоточном интерфейсе и таких вызовов одновременно будет очень мало (порядка одного, и редко больше), то для хранения объектов выбрана структура List. В функцию Lock передается объект, который блокируется и делегат, который должен быть выполнен, если объект не заблокирован.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class InterfaceLock
{
    #region Locker class
    private static List< Object >    _listOfLocks = new List< Object >();
    private class Locker : IDisposable
    {
        private Object _lockObj;
        
        //
        public Locker( Object obj )
        {
            _lockObj = obj;
            lock( m_listOfLocks )
                m_listOfLocks.Add( _lockObj );
        }
        public void Dispose()
        {
            lock( m_listOfLocks )
                m_listOfLocks.Remove( _lockObj );
        }
    }
    #endregion


    /// Блокировать код и выполнить его. Если объект уже заблокирован, то выйти.
    public static void        Lock( Object lockObj, Action lockedCode )
    {
        lock( m_listOfLocks )
        {
            if( null == lockObj || m_listOfLocks.Contains( lockObj ) )
                return;
        }

        // Выполнить код
        using( new Locker( lockObj ) )
        {
            lockedCode();
        }
    }
    #endregion
}
Публикация защищена лицензией CC BY 4.0 .