Как избежать рекурсивного запуска событий в WPF? - PullRequest
7 голосов
/ 26 августа 2010

У меня есть два WPF (из стандартного набора) виджетов A и B. Когда я изменяю какое-то свойство A, оно должно быть установлено на B, когда оно изменяется на B, оно должно быть установлено на A.

Теперь у меня есть эта уродливая рекурсия -> я меняю A, поэтому код меняет B, но поскольку B меняется, он меняет A, поэтому он меняет B ... У вас есть картинка.

Как избежать этой рекурсии самым «стандартным» способом? Наивное удаление и добавление обработчиков событий не работает, и проверка, является ли новое значение таким же, как старое значение, здесь не применима (из-за колебаний вычислений - я не устанавливаю одно и то же значение в A и B, но преобразовано) .

Фон

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

  • Я не писал эти виджеты, я просто обрабатывал события, вот и все
  • несмотря на заголовок «рекурсивный запуск», обработчики вызываются последовательно, поэтому у вас есть последовательность entry-exit-entry-exit-entry-exit, а не entry-entry-entry-exit-exit-exit

    и последнее, вероятно, наименее важное, но тем не менее

  • в данном конкретном случае у меня есть общий обработчик для A и B

A и B (в данном случае) являются наблюдателями прокрутки, и я стараюсь пропорционально поддерживать одинаковую позицию для них обоих. Проект (автор Карин Хубер) находится здесь: http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx

Событие запуска

Идея блокировки событий настолько популярна, что я добавил последовательность запуска событий, вот и мы:

  • меняю А
  • Обработчик называется
  • отключаю обработчик A
  • Я изменяю B (это сохраняется, но не срабатывает)
  • Я включаю обработчик A
  • теперь событие получает из очереди
  • Обработчик B называется
  • отключаю обработчик B
  • Я меняю A
  • ...

Как видите, это бесполезно.

Ответы [ 7 ]

3 голосов
/ 26 августа 2010

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

Однако могут быть ситуации, когда такие зависимости - единственный путь. В этом случае я бы предложил использовать закрытые флаги, указывающие, было ли изменение B вызвано изменением A. Примерно так ( updated ):

public class A
{
    private bool m_ignoreChangesInB = false;

    private void B_ChangeOccurred(object sender, EventArgs e)
    {
        if (!m_ignoreChangesInB)
        {
            // handle the changes...
        }
    }

    private void SomeMethodThatChangesB()
    {
        m_ignoreChangesInB = true;
        // perform changes in B...
        m_ignoreChangesInB = false;
    }
}

Тот же подход следует использовать в классе B. Однако этот подход не обрабатывает изменения из нескольких потоков. Если A или B могут быть изменены из нескольких потоков одновременно, вам придется использовать соответствующие методы, чтобы избежать потери изменений свойств.

2 голосов
/ 26 августа 2010

Вместо того чтобы вызывать события, проведите рефакторинг вашего кода, чтобы обработчики событий для A и B вызывали другой метод для выполнения фактической работы.

private void EventHandlerA(object sender, EventArgs e)
{
    ChangeA();
    ChangeB();
}

private void EventHandlerB(object sender, EventArgs e)
{
    ChangeB();
    ChangeA();
}

Затем вы можете расширить / изменить эти методы, если вам нужноделайте слегка разные вещи, если меняете A напрямую или через B.

ОБНОВЛЕНИЕ

Учитывая, что вы не можете изменить / не имеете доступа к коду, которого нетт решение.

0 голосов
/ 26 сентября 2015

Я не знаком с элементами управления WPF, поэтому следующие решения могут быть неприменимы:

  • обновлять, только если новое значение не совпадает со старым значением. Вы уже заявили, что это не относится к вашему случаю, так как значения всегда немного не совпадают.
  • хакерская установка логического флага при уведомлении, которое предотвращает повторение, если оно уже есть в уведомлении. Недостаток может заключаться в том, что некоторые уведомления пропускаются (то есть клиенты не обновляются), и этот флаг не работает, если уведомления публикуются (вместо того, чтобы напрямую вызываться).
  • элементы управления Windows делают различие между действиями пользователя, которые вызывают события, и настройкой данных программно. Последняя категория не уведомляет.
  • с использованием шаблона Observer (или Mediator), когда элементы управления не обновляют друг друга напрямую
0 голосов
/ 10 сентября 2010

Поскольку позиции масштабируются или иным образом изменяются между ScrollViewers, вы не можете использовать простую привязку, но будет ли работать конвертер?

<Window x:Class="Application1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SurfaceApplication21"
    Title="SurfaceApplication21"
    >
    <Window.Resources>
        <local:InvertDoubleConverter x:Key="idc" />
    </Window.Resources>
  <Grid>
        <StackPanel>
            <Slider Minimum="-100" Maximum="100" Name="a" Height="23" HorizontalAlignment="Left" Margin="30,12,0,0" VerticalAlignment="Top" Width="100" />
            <Slider Minimum="-100" Maximum="100" Value="{Binding ElementName=a, Path=Value, Converter={StaticResource idc}}" Name="b" Height="23" HorizontalAlignment="Left" Margin="30,12,0,0" VerticalAlignment="Top" Width="100" />
        </StackPanel>
    </Grid>
</Window>

код, в котором вы можете реализовать любую математику, необходимую для масштабирования между двумя представлениями.

[ValueConversion(typeof(double), typeof(double))]
public class InvertDoubleConverter : IValueConverter
{

    public object Convert(object value, Type targetType,
        object parameter, System.Globalization.CultureInfo ci)
    {
        return -(double)value;
    }

    public object ConvertBack(object value, Type targetType,
        object parameter, System.Globalization.CultureInfo ci)
    {
        return -(double)value;
    }
}

Я не пробовал с ScrollViewers, но поскольку полосы прокрутки, являющиеся частью шаблона ScrollViewer, и ползунки оба происходят от RangeBase, что-то вроде этого должно работать, но вам, возможно, придется повторно шаблонировать и / или создавать подклассы ваших ScrollViewers. 1007 *

0 голосов
/ 27 августа 2010

Немного хакерское решение - установить период времени отключения, в течение которого вы игнорируете последующие срабатывания обработчика событий. Если вы это сделаете, обязательно используйте DateTime.UtcNow вместо DateTime.Now, чтобы избежать граничного условия для перехода на летнее время.

Если вы не хотите зависеть от времени (события могут законно обновляться быстро), вы можете использовать подобный тип блокировки, хотя это еще более хакерский:

        private int _handlerCounter;
        private void Handler()
        {
            //The logic of this handler will trigger an 'asynchronously reentrant' callback, so we ignore the next (and only the next) callback
            //Note that this breaks down if the callback is not triggered, so we need to make certain the reentrancy will occur
            //If we can't ensure that, we need to at least detect that it won't occur and manually decrement the counter
            if (Interlocked.CompareExchange(ref _handlerCounter, 0, 1) == 0)
            {
                //Call set on A/B, which triggers callback of this same handler for B/A
            }
            else
            {
                Interlocked.Decrement(ref _handlerCounter);
            }
        }
0 голосов
/ 27 августа 2010

и проверка, совпадает ли новое значение со старым, здесь не применима (из-за колебаний расчета - я не устанавливаю то же значение в A и B, но преобразую).

Итак, вы говорите, что происходит нечто подобное:

  1. A.Foo устанавливается на x .
  2. A_FooChanged устанавливает B.Bar в f ( x ).
  3. B_BarChanged устанавливает A.Foo в g (f ( x )), что не x .
  4. A_FooChanged устанавливает B.Bar в f (g (f ( x ))).

и так далее.Это правильно?Потому что если g (f ( x )) равно x , то решение простое: B_BarChanged следует установить только A.Foo, если A.Foo! =g (f ( x ).

Если предположить, что у этой рекурсии нет вычисляемого конечного состояния, то обработчикам событий требуется какой-то способ узнать контекст, в котором были сработаны обрабатываемые ими события).Вы не можете получить эту информацию из обычного протокола событий, потому что события предназначены для отделения операций, которые этот дизайн связывает.

Звучит так, как будто вам нужен внеполосный способ, чтобы эти элементы управления сигнализировалидруг другу. Это может быть так же просто, как использовать HashSet<EventHandler>, это свойство Window. Я хотел бы рассмотреть что-то вроде этого:

private void A_FooChanged(object sender, EventArgs e)
{
   if (!SignalSet.Contains(B_BarChanged))
   {
      SignalSet.Add(A_FooChanged);
      B.Bar = f(A.Foo);
      SignalSet.Remove(A_FooChanged);
   }
}

Это ломается, если A устанавливает B.Bar, и B устанавливает C.Baz, а C устанавливает A.Foo, хотя я подозреваю, что сами требования нарушаются, если это происходит. В этом случае вам, вероятно, придется прибегнуть к просмотру трассировки стека. Это не красиво, но потомНичто в этой проблеме не очень красиво.

0 голосов
/ 26 августа 2010

Решение ChrisFs, вероятно, является подходящим вариантом, но иногда мы удаляем событие, вносим изменения и повторно добавляем обработчик события:

Представьте DataContext с событием DataContextChanged:

DataContextChanged -= OnDataContextChanged;
DataContext = new object();
DataContextChanged += OnDataContextChanged;

Таким образом, вы точно будете знать, что ваш обработчик событий не отключится, так сказать. Событие все еще происходит однако;).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...