【问题标题】:How to avoid recursive triggering of events in WPF?如何避免WPF中事件的递归触发?
【发布时间】:2011-04-04 05:37:34
【问题描述】:

我有两个 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(在这种情况下)是滚动查看器,我尝试按比例为它们保持相同的位置。该项目(由 Karin Huber 设计)在这里: http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx

事件触发

阻塞事件的想法非常流行,我添加了触发事件的顺序,我们开始:

  • 我换A了
  • 处理程序被调用
  • 我禁用了 A 的处理程序
  • 我更改 B(已存储,但未触发)
  • 我启用 A 的处理程序
  • 现在从队列中获取事件
  • B 处理程序被调用
  • 我禁用 B 的处理程序
  • 我换A了
  • ...

如你所见,这是徒劳的。

【问题讨论】:

    标签: c# wpf events recursion


    【解决方案1】:

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

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

    如果您需要做一些微妙的不同的事情,如果直接或通过 B 更改 A,那么您可以扩展/更改这些方法。

    更新

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

    【讨论】:

    • 不可行——这是WPF,A和B是widget,widget内部触发事件。
    • 但是事件与事件处理程序挂钩,您肯定可以影响那些内部发生的事情,不是吗?
    • Chrisian,当然,但我只能调用小部件设计者公开的内容。因此,一旦我更改 B,它就会触发事件,这会触发我的处理程序....并且我们有“递归”(也许我应该把它放在引号中以强调,这不是真正的递归)。
    【解决方案2】:

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

    但是,在某些情况下,这种依赖关系可能是唯一可行的方法。在这种情况下,我建议使用私有标志来指示 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,则必须使用适当的技术来避免丢失属性更改。

    【讨论】:

    • 我强调了这些是 WPF 小部件,而不是我的 - 即它们来自标准集,我只是使用它们。只有事件处理程序是我的,其余的不是自定义的 WPF。而且它不起作用,因为递归触发只是我们的错觉——事件是按顺序处理的。
    • 这没关系。最后,有问题的情况是当您的代码 更改属性时。也许,我的代码示例不完全符合这种情况。但是有一个标志表明值更改是由您的代码启动的想法仍然有效。
    • 我不工作 - 查看事件处理方式的“跟踪”。如果在更改后立即处理事件,它会很好用。但他们不是,你的标志被恢复回来,然后再次调用处理程序。
    • 好的,现在我明白了。你知道为什么不立即调用处理程序吗?属性更改是否在另一个线程上执行?
    • 是的,这些属性是 WPF 小部件的,这是特定的线程。顺便提一句。作为解决方法,可能会有类似的东西——存储应该阻止哪些事件的信息(独立于 WPF)。因此,当 A 更改 B 时,它应该添加到“忽略列表”B,并且当 B 处理程序执行时,它应该从该列表中删除自己。所以调用者总是添加自己,但删除所有其他的。
    【解决方案3】:

    ChrisFs 解决方案可能是要走的路,但有时我们删除事件,进行更改,然后重新添加事件处理程序:

    想象一个带有 DataContextChanged 事件的 DataContext:

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

    这样,您就可以肯定地知道您的事件处理程序不会关闭。但是,该事件仍然会触发;)。

    【讨论】:

    • 我已经写过了——这行不通。事件是按顺序触发的,递归只是一种错觉,因为事件是排队的。
    • 为什么这不起作用?如果你知道 B 会调用 A,你可以在 B 中解开 A,做你的事情并重新钩住 A。
    • 因为事件处理程序不可重入。它们是按顺序处理的——我现在在帖子中添加了示例。
    • 我不建议你在handler A中去掉A的handler,我建议去掉B的handler中的handler A:#我改了A#A handler被调用#我禁用了handler B # 我更改了 B(这是存储的,但未触发) # 我启用 B 的处理程序 # 希望 B 处理程序不会被调用。否则,请考虑在后台工作人员中进行更改。
    • 实际上,我做了更多——我删除了所有的处理程序 :-) 但它没有用(在跟踪调用之后,我知道为什么了)。
    【解决方案4】:

    这里不适用检查新值是否与旧值相同(因为计算的波动——我不是将相同的值设置为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)))。

    等等。它是否正确?因为如果 g(f(x)) is x,那么解决方案很简单:B_BarChanged 应该只设置 A.Foo if @987654330 @ != 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,这将崩溃,但我怀疑如果发生这种情况,需求本身就会崩溃。在这种情况下,您可能不得不求助于查看堆栈跟踪。这不漂亮,但是这个问题一点也不漂亮。

    【讨论】:

    • 感谢您的创意!关于函数,是的 g(f(x)) 不必是 x (可以,但我不能假设)。我想到了对象和事件的字典,但我放弃了这种方式 - 问题是当 g(f(x)) IS x (我无法预测它,至少不是干净的方式)。当新值与旧值不同时触发事件 - 使用这本字典,我将等待事件的到来,但它永远不会到来,因为值相等。
    • 最后一句没看懂。在我描述的场景中没有“等待事件”:B_BarChanged 检查HashSet。如果A_FooChangedHashSet 中,那么B_BarChanged 知道它是在A.Foo 被更改的上下文中调用的,它不应该更改A.Foo
    • 看,你触发了 A,所以你更新了 B——所以你将 B 设置为下次忽略。但是你不知道 B 不会被更新,因为旧值 = 新值。一段时间后 B 将被触发(由于用户交互),现在它将被忽略,因为这是您第一次从 B 获取事件。
    • 所以检查一下是否f(A.Foo) == B.Bar,如果没有则只进行更新。
    【解决方案5】:

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

    【讨论】:

    • 谢谢,但我不会朝那个方向走——它会带来很多麻烦。较慢/较快的计算机,一切都会崩溃。
    【解决方案6】:

    因为位置是按比例缩放的,或者在 ScrollViewer 之间发生了其他变化,所以您不能使用简单的绑定,但是转换器可以工作吗?

    <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 模板和 Sliders 中的滚动条都是从 RangeBase 下降的,因此应该可以使用类似的方法,但您可能需要重新模板化和/或子类化 ScrollViewers。

    【讨论】:

    • 但它并没有真正改变。转换器改变输出值,输出值触发器再次改变——这次回来了。您展示了具有可逆函数(数学否定)的示例,这就是它起作用的原因,而不是因为转换器。要查看每次转换的不可逆函数的效果,请添加一个随机数 (-10,+10)。
    【解决方案7】:

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

    • 仅当新值与旧值不同时才更新。您已经说过这不适用于您的情况,因为这些值总是略有偏差。
    • hack 在通知时保留布尔标志,如果已经在通知中,则防止递归。缺点可能是错过了一些通知(即客户端未更新),并且如果发布通知(而不是直接调用),此标志不起作用。
    • Windows 控件区分引发事件的用户操作和以编程方式设置数据。最后一个类别不通知。
    • 使用观察者(或中介者)模式,其中控件不直接相互更新

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-06-05
      • 1970-01-01
      • 2017-02-03
      • 2023-03-19
      • 2011-02-21
      • 2021-05-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多