TextBox不会重新绑定到ObservableCollection替换的对象

时间:2014-04-12 18:02:41

标签: c# wpf binding

当我绑定到ObservableCollection时,我的行为很奇怪。如果集合在设置值期间通过TextBox中的绑定发生更改,则TextBox将不会绑定到新对象,但仍保持绑定到旧对象(现在无效)。

重现问题的详细说明

  1. 启动应用
  2. 修改您选择的项目TextBox(然后点击此处,以便将更改更新为源代码)
  3. ItemVM的设置者更新了支持集合,反过来触发事件通知View该项目已被替换
  4. TextBlock其他 TextBox成功反弹到集合中的新项目,但您用来编辑此项目的TextBox仍然是绑定到旧的ItemVM
  5. 如果您尝试使用相同 TextBox再次编辑值,则会失败(因为该项目中不再存在该项目)
  6. 如果您尝试使用其他 TextBox编辑值,则第一个TextBox会反弹并再次运行,但现在此TextBox已损坏
  7. 您可以编辑TextBox es想要多少次,但必须始终在它们之间交替
  8. 我认为,这是因为原始TextBox正在执行代码,所以它忽略了更改后的值,一旦完成执行该代码,它就会再次侦听绑定事件并简单地将自身重新绑定到新对象。

    问题是:如何防止这种情况?

    PS:这个例子真的很简单,在实际情况下ItemVM不直接更新集合,而是一些通用对象 - 直到运行时才知道,所以类型为&的建议#34;不要在那里更新集合"是无关紧要的。

    请随意询问,我会尝试添加其他信息。

    完整的源代码

    代码

    using System;
    using System.Windows;
    using System.ComponentModel;
    using System.Collections.ObjectModel;
    
    namespace WpfApplication1 {
    
        class Program {
    
            [STAThread]
            static void Main() {
                var dc = new VM {
                    Items = new ObservableCollection<ItemVM>(),
                };
    
                for(int i = 0; i < 10; i++) {
                    dc.Items.Add(new ItemVM(i.ToString(), dc));
                }
    
                var w = new MainWindow {
                    DataContext = dc,
                };
    
                new Application().Run(w);
            }
        }
    
        class VM {
            public ObservableCollection<ItemVM> Items { get; set; }
        }
    
        class ItemVM : INotifyPropertyChanged {
            public event PropertyChangedEventHandler PropertyChanged;
    
            public VM ParentVM { get; set; }
    
            string value;
            public string Value {
                get { return value; }
                set {
                    int index = ParentVM.Items.IndexOf(this);
                    ParentVM.Items[index] = new ItemVM(value, ParentVM);
                    this.value = value + " in old VM";
    
                    var h = PropertyChanged;
                    if(h != null)
                        h(this, new PropertyChangedEventArgs("Value"));
                }
            }
    
            public ItemVM(string value, VM parent) {
                this.value = value;
                ParentVM = parent;
            }
        }
    }
    

    XAML

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
    
        <ListBox ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Blue" BorderThickness="2">
                        <StackPanel>
                            <TextBlock Text="{Binding Value}" />
                            <TextBox Text="{Binding Value}" />
                            <TextBox Text="{Binding Value}" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
    
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </Window>
    

2 个答案:

答案 0 :(得分:1)

您正在完全更改DataContext TextBox所绑定的public string Value { get { return value; } set { int index = ParentVM.Items.IndexOf(this); App.Current.Dispatcher.BeginInvoke((Action)delegate <-- HERE { ParentVM.Items[index] = new ItemVM(value, ParentVM); }); this.value = value + " in old VM"; var h = PropertyChanged; if (h != null) { h(this, new PropertyChangedEventArgs("Value")); } } } 。所以,我怀疑在更新早期DataContext的绑定属性时,textBox无法选择新的DataContext。

您可以做的是异步更新新的DataContext在UI调度程序上,以便所有UI组件都可以获取DataContext中的更新,包括正在获取更新的更新。即,异步地在UI调度程序上对集合更新代码进行排队。

{{1}}

答案 1 :(得分:0)

Rohit的解决方案有效,但之后无法使用验证。我想出了两个解决方案。

使用Binding.IsAsync

<TextBox Text="{Binding Value, IsAsync=True}" />

它与在属性setter中异步调用的工作方式基本相同。由于这是内置功能,因此更清楚作者的意图是什么,因此更容易维护。

此外,从.NET 4.5开始,我们可以实现接口INotifyDataErrorInfo,并且即使在异步绑定上也可以进行验证。

异步提升ObservableCollection的事件

此解决方案适用于.NET 4.5之前的版本,除了ValidatesOnExceptions之外,还允许使用ValidatesOnDataErrors,这是第一个解决方案的唯一选项。

同步调用属性setter,同步使用绑定(IsAsync=False)。区别在于绑定的ObservableCollection必须异步调用事件。可以通过在类中包装ObservableCollection来完成它:

class MarshaledObservableCollection<T> : IEnumerable<T>, INotifyCollectionChanged, IWeakEventListener {
    readonly IEnumerable<T> backingCollection;

    public event NotifyCollectionChangedEventHandler CollectionChanged {
        add {
                var info = new NotifyCollectionChangedEventHandlerInfo {
                Handler = value,
                Dispatcher = Dispatcher.CurrentDispatcher,
            };
            collectionChangedHandlers.Add(info);
        }
        remove {
            var info = new NotifyCollectionChangedEventHandlerInfo {
                Handler = value,
                Dispatcher = Dispatcher.CurrentDispatcher,
            };
            collectionChangedHandlers.Remove(info);
        }
    }

    readonly List<NotifyCollectionChangedEventHandlerInfo> collectionChangedHandlers = new List<NotifyCollectionChangedEventHandlerInfo>();

    public MarshaledObservableCollection(IEnumerable<T> collection) {
        if (collection == null)
            throw new ArgumentNullException("collection", "collection is null.");

        var obs = collection as INotifyCollectionChanged;
        if (obs == null)
            throw new ArgumentException("collection must be INotifyCollectionChanged", "collection");

        backingCollection = collection;
        CollectionChangedEventManager.AddListener(obs, this);
    }

    public IEnumerator<T> GetEnumerator() {
        return backingCollection.GetEnumerator();
    }

    protected virtual void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
        foreach (var item in collectionChangedHandlers) {
            //Important line - invoking asynchronously with the DataBind priority
            item.Dispatcher.BeginInvoke(item.Handler, DispatcherPriority.DataBind, sender, e);
        }
    }

    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) {
        if (managerType == typeof(CollectionChangedEventManager)) {
            OnCollectionChanged(sender, (NotifyCollectionChangedEventArgs)e);
            return true;
        } else {
            return false;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }

    struct NotifyCollectionChangedEventHandlerInfo {
        public Dispatcher Dispatcher { get; set; }
        public NotifyCollectionChangedEventHandler Handler { get; set; }
    }
}