WPF:PropertyChangedCallback仅触发一次

10
我有一个用户控件,其中公开了名为“VisibileItems”的DependencyProperty。 每次该属性被更新时,我都需要触发另一个事件。 为了实现这一点,我添加了一个具有PropertyChangedCallback事件的FrameworkPropertyMetadata。
由于某种原因,这个事件只会被调用一次,并且不会在下一次VisibleItems更改时触发。
XAML:
<cc:MyFilterList VisibleItems="{Binding CurrentTables}"  />

CurrentTables是MyViewModel中的一个依赖属性。CurrentTables经常发生变化。我可以将另一个WPF控件绑定到CurrentTables,并在UI中看到更改。

这是我使用PropertyChangedCallback连接VisibleItems的方式

public static readonly DependencyProperty VisibleItemsProperty =
    DependencyProperty.Register(
    "VisibleItems",
    typeof(IList),
    typeof(MyFilterList),
    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(VisiblePropertyChanged))

    );

public IList VisibleItems {
    get { return (IList)GetValue(VisibleItemsProperty); }
    set { SetValue(VisibleItemsProperty, value); }
}

通过进入“VisiblePropertyChanged”方法,我可以看到它在第一次设置“CurrentTables”时被触发,但是在随后的更改中没有被触发。

更新:

由于有些人质疑了修改“CurrentTables”的方式,实际上每次更改时都会完全重新分配它:

OnDBChange()...
CurrentTables = new List<string>(MainDatabaseDataAdapter.GetTables(this.SelectedServer, this.SelectedDatabase));

这行代码会在每次修改时被调用,但是我的VisiblePropertyChanged处理程序仅在第一次被调用。

更新:

如果我直接赋值VisibleItems,处理程序就会每次都被调用!

TestFilterList.VisibleItems = new List<string>( Enumerable.Range(1, DateTime.Now.Second).ToList().Select(s => s.ToString()).ToList() );

看起来问题出在 DependencyProperty (VisibleItems) 监听另一个 DependencyProperty (CurrentTables) 上。一些绑定只在首次属性更改时生效,但在随后的更改中不起作用?尝试使用一些人建议的 Snoop 来检查此问题。


你确定绑定仍然完好无损吗?我会使用Snoop来验证绑定是否正确更新了VisibleItems属性。 - default.kramer
你在暗示但并不明确:你确定 VisibleItems 属性在没有调用属性更改回调函数的情况下实际上会更改为另一个值吗? - Rick Sladkey
Rick,是的,我确定可以逐步调试并查看它被调用,现在每次调用我都创建一个“新列表”,以确保它不是相同的实例。但是,如果您看一下代码,您会发现它每次都从数据库获取列表。 - Sonic Soul
1
证明 CurrentTables 在改变并不意味着 VisibleItems 也在改变。你的绑定可能已经失效了或者出了什么问题。我怀疑 VisibleItems 实际上没有改变,所以我建议使用 Snoop 确保绑定正常工作。 - default.kramer
克莱默,干得好。我进行了另一项测试,似乎问题确实出现在VisibleItems和CurrentTables之间...需要进行一些调查,看看为什么它们第一次尝试时可以工作,但后续却不行。 - Sonic Soul
4个回答

17
你是否正在为一个同时具有OneWay绑定的依赖属性设置“本地”值(即直接分配给依赖属性setter)?如果是,设置本地值将会移除绑定,正如在MSDN依赖属性概述中所提到的:

绑定被视为本地值,这意味着如果你设置另一个本地值,你将消除绑定。

当依赖属性机制被要求在依赖属性上存储本地值时,它没有太多其他的选择。它不能通过绑定发送值,因为绑定“指向”的方向是错误的。在被设置为本地值之后,它不再显示从绑定获取的值。由于它不再显示来自绑定的值,它会删除绑定。
一旦绑定被删除,当绑定的源属性更改其值时,PropertyChangedCallback将不再被调用。这可能是回调未被调用的原因。
如果将绑定设置为TwoWay,绑定系统确实有一个地方可以存储您设置的“本地”值:在绑定的源属性中。在这种情况下,没有必要消除绑定,因为依赖属性机制可以将值存储在源属性中。
这种情况不会导致堆栈溢出,因为以下情况发生:
  • 依赖属性接收“本地”值。
  • 依赖属性机制沿着绑定向后发送值到源属性,
  • 源属性设置属性值并触发PropertyChanged
  • 依赖属性机制接收到PropertyChanged事件,检查源属性的新值,发现它没有改变,因此不再执行任何操作。

关键点在于,如果您为一个属性触发了PropertyChanged事件,但这个属性的值没有变化,那么绑定到该属性的任何依赖属性上的PropertyChangedCallback都不会被调用。

为了简单起见,在上述内容中我忽略了IValueConverter。如果您确实有转换器,请确保它正确地在两个方向上进行值转换。我还假设另一端的属性是实现了INotifyPropertyChanged接口的对象上的视图模型属性。源端的绑定可能还有另一个依赖属性。依赖属性机制也可以处理它。

恰好WPF(和Silverlight)不包含堆栈溢出的检测。如果在PropertyChangedCallback中,您将依赖属性的值设置为与其新值不同的值(例如通过递增整数值属性或将字符串附加到字符串值属性),则会出现堆栈溢出。


1
不,不会在事件中设置VisibleItems。该事件执行翻译并设置我的用户控件上的另一个属性。 - Sonic Soul
@Luke Woodward:千言万语感谢!您的帖子至少为我节省了一天的调试时间。 - Dimitri C.

5

我在我的代码中遇到了同样的问题,Luke说得对。我错误地在PropertyChangedCallback内调用了SetValue,导致潜在的无限循环。WPF会静默地禁用回调函数来防止这种情况发生!

我的WPF用户控件如下:

PatchRenderer

我的C#属性是Note:

    [Description("Note displayed with star icons"), 
    Category("Data"),
    Browsable(true), 
    EditorBrowsable(EditorBrowsableState.Always),
    DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public int Note
    {
        get { return (int)GetValue(NoteProperty); }
        set { SetValue(NoteProperty, value); /* don't put anything more here */ }
    }

我的 WPF 属性
public static readonly DependencyProperty 
        NoteProperty = DependencyProperty.Register("Note",
        typeof(int), typeof(PatchRenderer),
        new PropertyMetadata(
            new PropertyChangedCallback(PatchRenderer.onNoteChanged)
            ));

    private static void onNoteChanged(DependencyObject d,
               DependencyPropertyChangedEventArgs e)
    {
        // this is the bug: calling the setter in the callback
        //((PatchRenderer)d).Note = (int)e.NewValue;

        // the following was wrongly placed in the Note setter.
        // it make sence to put it here.
        // this method is intended to display stars icons
        // to represent the Note
        ((PatchRenderer)d).UpdateNoteIcons();
    }

1
您可能遇到这样的问题:集合中的内容发生了改变,但实例并没有改变。在这种情况下,您需要使用ObservableCollection并执行类似于以下代码的操作:
public static readonly DependencyProperty VisibleItemsProperty =
    DependencyProperty.Register(
    "VisibleItems",
    typeof(IList),
    typeof(MyFilterList),
    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(VisibleItemsChanged)));

    private static void VisibleItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var myList = d as MyFilterList;
        if (myList == null) return;

        myList.OnVisibleItemsChanged(e.NewValue as IList, e.OldValue as IList);
    }

    protected virtual void OnVisibleItemsChanged(IList newValue, IList oldValue)
    {
        var oldCollection = oldValue as INotifyCollectionChanged;
        if (oldCollection != null)
        {
            oldCollection.CollectionChanged -= VisibleItems_CollectionChanged;
        }
        var newCollection = newValue as INotifyCollectionChanged;
        if (newCollection != null)
        {
            newCollection.CollectionChanged += VisibleItems_CollectionChanged;
        }
    }

该集合正在被重复重新分配。 - Sonic Soul
1
你能在你原来的问题中再贴一些代码展示这个吗? - bendewey

1

如果您只是通过代码实例化一个MyFilterList并设置VisibleItems,就像这样:

var control = new MyFilterList();
control.VisibleItems = new List<string>();
control.VisibleItems = new List<string>();

你很可能会看到每次发生 PropertyChangedCallback。这意味着问题出在绑定上,而不是回调函数上。确保没有绑定错误,你正在引发 PropertyChanged,并且没有破坏绑定(例如通过在代码中设置 VisibleItems)。


没错。我直接分配了那个属性,它每次都有效...所以问题似乎源于一个 dep 属性观察另一个 dep 属性...不知何故,变更通知只发生一次... - Sonic Soul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接