如何避免WPF中事件的递归触发?

时间:2010-08-26 11:42:12

标签: c# wpf events recursion

我有两个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 < / p>

    和最后一个,可能是最不重要的,但不过

  • 在这种特殊情况下,我有A和B的通用处理程序

A和B(在这种情况下)是滚动查看器,我尝试按比例保持两个相同的位置。该项目(由Karin Huber提供)在这里: http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx

事件触发

阻止事件的想法如此受欢迎,我添加了触发事件的顺序,我们走了:

  • 我改变了A
  • 称为处理程序
  • 我禁用了A
  • 的处理程序
  • 我改变B(存储但未触发)
  • 我启用了A
  • 的处理程序
  • 现在事件是从队列中获取的
  • 称为B处理程序
  • 我禁用了B
  • 的处理程序
  • 我改变了A
  • ...

如你所见,这是徒劳的。

7 个答案:

答案 0 :(得分:3)

首先,我会考虑设计,因为这些循环依赖通常是糟糕设计的标志。

但是,可能存在这种依赖性是唯一的方法。在这种情况下,我建议使用私有标志来指示B的变化是否是由A的变化引起的。像这样(更新):

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,则必须使用适当的技术来避免属性更改丢失。

答案 1 :(得分:2)

不是引发事件,而是重构代码,以便A和B的事件处理程序调用另一种方法来完成实际的工作。

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

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

如果您需要在直接或通过B更改A时需要进行微妙的不同操作,则可以扩展/更改这些方法。

<强>更新

鉴于您无法更改/无法访问代码,这不是解决方案。

答案 2 :(得分:0)

ChrisFs解决方案可能就是这样,但有时我们会删除事件,进行更改并重新添加事件处理程序:

想象一下DataContext,使用DataContextChanged事件:

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

通过这种方式,您可以确定您的事件处理程序不会消失。然而事件仍然会发生;)。

答案 3 :(得分:0)

  

并检查新值是否与旧值相同在此处不适用(因为计算的波动 - 我没有将相同的值设置为A和B,但已转换)。

所以你说的是这样的事情发生了:

  1. A.Foo设置为 x
  2. A_FooChangedB.Bar设置为f( x )。
  3. B_BarChangedA.Foo设置为g(f( x )),这不是 x
  4. A_FooChangedB.Bar设置为f(g(f( x )))。
  5. 等等。它是否正确?因为如果g(f( x )) x ,那么解决方案的简单:B_BarChanged应该只设置A.Foo } if 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,则会出现故障,但我怀疑如果发生这种情况,要求本身就会崩溃。在这种情况下,您可能不得不求助于查看堆栈跟踪。那不是很好,但是这个问题没什么了不起。

答案 4 :(得分:0)

稍微有点狡猾的解决方案是设置一个中断时间段,在此期间忽略后续的事件处理程序激活。如果您这样做,请务必使用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);
            }
        }

答案 5 :(得分:0)

因为在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模板的Scrollbars和Sliders都来自RangeBase,这样的东西应该可以工作,但你可能需要重新模板化和/或子类化你的ScrollViewers。 / p>

答案 6 :(得分:0)

我不熟悉WPF控件,因此以下解决方案可能不适用:

  • 仅在新值与旧值不同时更新。您已经声明这不适用于您的案例,因为值总是稍微关闭。
  • 在通知时保留布尔标志的黑客,如果已经在通知中则通知哪个会阻止递归。缺点可能是错过了某些通知(即客户端未更新),并且如果发布通知(而不是直接调用),则此标志不起作用。
  • Windows控件区分引发事件的用户操作和以编程方式设置数据。最后一类不通知。
  • 使用Observer(或Mediator)模式,其中控件不会直接相互更新