【问题标题】:ElementName bindings not working reliably - suspected dispatcher race conditionElementName 绑定不能可靠地工作 - 可疑的调度程序竞争条件
【发布时间】:2012-09-21 15:47:43
【问题描述】:

Download repro project

编辑

有时工作的解决方案完全让我摆脱了最初的疑惑,为什么它首先会起作用。毕竟,这些项目不是可视树的一部分。最后,这是完全有道理的:

  • 该集合中的按钮不在可视树中,因此元素绑定不起作用。
  • 应用模板会将它们放入可视化树并绑定,如果此时应用,则开始工作。
  • 这证实了可疑的竞争条件。

我的一位同事进行了一些扩展调试,也显示了该问题 - 在绑定成功的情况下,首先调用 OnApplyBinding。所以在不调整逻辑树的情况下使用集合是有缺陷的。

感谢您回到正轨的回复!


原帖

我有一个公开 ObservableCollection 的视图控件,我的视图可以包含任意元素,例如纽扣。注意按钮上的 ElementName 绑定:

<local:ViperView>    
    <local:ViperView.MenuItems>
        <Button Content="{Binding ElementName=btn, Path=Content}" />
    </local:ViperView.MenuItems>

    <Grid>
        <Button x:Name="btn" Content="HELLO WORLD" />
    </Grid>
</local:ViperView>

控件的 ControlTemplate 只是使用 ItemsControl 呈现内容:

<ControlTemplate ...
...

    <ItemsControl
        x:Name="PART_NavigationMenuItemsHost"
        ItemsSource="{Binding MenuItems, RelativeSource={RelativeSource TemplatedParent}}" />
 </ControlTemplate>

上面的视图分配给我的主视图模型的 ActiveView 属性。主窗口只是通过数据绑定显示视图。

现在的问题:如果视图在创建后没有立即分配给视图模型,则该视图的 ElementName 绑定无法可靠地工作。

ElementName 绑定的工作方式如下:

MainViewModel.ActiveView = new ViperView();

ElementName 绑定有时使用正常优先级:

var view = new ViperView();
Dispatcher.BeginInvoke(() => MainViewModel.ActiveView  view);
如果视图模型属性设置为低优先级,

ElementName 绑定总是失败

var view = new ViperView();
Dispatcher.BeginInvoke(DispatcherPriority.Render, () => MainViewModel.ActiveView = view);
如果属性是从工作线程设置的,

ElementName 绑定有时 有效(绑定引擎编组回 UI 线程):

var view = new ViperView();
Task.Factory.StartNew(() => MainViewModel.ActiveView = view);

ElementName 绑定总是失败如果工作线程有延迟:

var view = new ViperView();
var view = new ViperView();
Task.Factory.StartNew(() => 
{
    Thread.Sleep(100);
    MainViewModel.ActiveView = view;
});

我对此没有答案。这似乎与时间有关。例如,如果我在上面的任务示例中添加一个简短的 Thread.Sleep,这 总是 会导致绑定中断,而没有睡眠,它只是有时会中断。

这对我来说是一个很好的展示 - 任何指针都值得赞赏......

感谢您的建议 菲利普

【问题讨论】:

    标签: wpf data-binding itemscontrol dispatcher


    【解决方案1】:

    据我所知,ElementName 绑定不会随时更新:它只会绑定到属性一次,然后停止更新。 这可以在这里解释您的问题:第一次绑定将根据时间戳发生(或不会发生)。

    您可以通过为绑定指定UpdateSourceTrigger 属性来修复它:

    <Button Content="{Binding ElementName=btn, Path=Content, UpdateSourceTrigger=PropertyChanged}" />
    

    这将确保每次更新 btn.Content 时您的绑定都会更新。

    希望这可行 =)

    【讨论】:

    • 据我所知,你错了:P。默认情况下,该绑定是“PropertyChanged”。
    • @ChrisDD:你们都错了。 1. UpdateSourceTrigger 对于不自己改变属性值的控件是没有意义的。 2.如果绑定不是TwoWayOneWayToSourceUpdateSourceTrigger也没有意义,在这种情况下也不是。
    【解决方案2】:

    我不能完全解释为什么第一个选项有效。但是我可以解释为什么其他的不起作用。

    好的,首先,ElementName 只能在元素在同一个可视化树中时起作用。请注意,NavigationButtonItems 与 ViperView 的实际内容是分开的。

    所以说你这样做:

    <Button Content="{Binding ActualWidth, 
                RelativeSource={RelativeSource AncestorType=WpfApplication2:ViperView}}" />
    

    (它是 NavigationButtonItems 的一部分)。现在,如果 NavigationButtonItems 和 ViperView 项没有混合为一个(在 ControlTemplate 中混合到一个可视化树),则此绑定将失败,并且保持失败。

    现在假设在绑定发生时可视化树恰好是 ONE,那么绑定就会成功,一切都很好。

    请注意,当您渲染内容时,会混合到一个可视化树中,例如: dc.ActiveScreen = viperview;

    这里有一个简单的例子来演示如何做得更好:

      <Button
    
                Background="Purple"
                Width="100"
                Height="20"
                 >
                <Button.Style>
                    <Style TargetType="Button">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding IsReady}" Value="True">
                                <Setter Property="Content" Value="{Binding ActualWidth, 
                RelativeSource={RelativeSource AncestorType=WpfApplication2:ViperView}}" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </Button.Style>
            </Button>
    

    IsReady 属性应该在 viewModel 中,它本质上告诉“是的,现在所有内容都呈现为一棵可视化树,您可以应用绑定。”

    如果您使用 IsReady 技巧,ActualWidth 将开始工作:

        Task.Factory.StartNew(() =>
                                  {
                                      Thread.Sleep(10);
                                      dc.ActiveScreen = view;
                                      //ps you might need to force wpf finish with  rendering here. like use Application.DoEvents()
                                      dc.IsReady = true;//force bindings since everything is one now.
                                  });
    

    如果您需要澄清,请告诉我。

    【讨论】:

    • 我也很惊讶 ElementBindings 在旧的、同步工作的 shell 中首先工作,但它们确实如此可靠。我的问题是我不能只更改现有按钮 - 该示例是一个相当复杂的应用程序架构的简化版本,它为数百个视图提供集成层 - 其中包含部分绑定。我也不能要求开发人员创建如此复杂的绑定。我想,对于新屏幕,我应该首先将 Items 集合设为 ItemsControl(或自定义项目控件),以便它可视化树的一部分。
    【解决方案3】:

    鉴于 ElementName 绑定在示例中的按钮甚至添加到父视图的集合之前部分失败,因此我无法拦截绑定。一个稍微肮脏的解决方法是在应用模板并建立可视化树后刷新这些控件中的绑定:

    从 OnApplyTemplate 开始,调用:

    internal static class BindingUtil
    {
        /// <summary>
        /// Recursively resets all ElementName bindings on the submitted object
        /// and its children.
        /// </summary>
        public static void ResetElementNameBindings(this DependencyObject obj)
        {
            IEnumerable boundProperties = obj.GetDataBoundProperties();
            foreach (DependencyProperty dp in boundProperties)
            {
                Binding binding = BindingOperations.GetBinding(obj, dp);
                if (binding != null && !String.IsNullOrEmpty(binding.ElementName)) //binding itself should never be null, but anyway
                {
                    //just updating source and/or target doesn’t do the trick – reset the binding
                    BindingOperations.ClearBinding(obj, dp);
                    BindingOperations.SetBinding(obj, dp, binding);
                }
            }
    
            int count = VisualTreeHelper.GetChildrenCount(obj);
            for (int i = 0; i < count; i++)
            {
                //process child items recursively
                DependencyObject childObject = VisualTreeHelper.GetChild(obj, i);
                ResetElementNameBindings(childObject);
            }
        }
    
    
        public static IEnumerable GetDataBoundProperties(this DependencyObject element)
        {
            LocalValueEnumerator lve = element.GetLocalValueEnumerator();
    
            while (lve.MoveNext())
            {
                LocalValueEntry entry = lve.Current;
                if (BindingOperations.IsDataBound(element, entry.Property))
                {
                    yield return entry.Property;
                }
            }
        }
    }
    

    另一个可能更可取的解决方法是在运行时更改逻辑树。将下面的代码添加到我的视图中也可以解决问题:

    public class ViperView : ContentControl
    {
        private readonly ObservableCollection<object> menuItems = new ObservableCollection<object>();
    
        public ObservableCollection<object> NavigationMenuItems
        {
            get { return menuItems; }
        }
    
    
        public ViperView()
        {
            NavigationMenuItems.CollectionChanged += OnMenuItemsChanged;
        }
    
        private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (var newItem in e.NewItems)
                {
                    AddLogicalChild(newItem);
                }
            }
        }
    
        protected override IEnumerator LogicalChildren
        {
            get
            {
                yield return this.Content;
                foreach (var mi in NavigationMenuItems)
                {
                    yield return mi;
                }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2013-04-13
      • 1970-01-01
      • 2021-12-06
      • 1970-01-01
      • 1970-01-01
      • 2017-07-17
      • 2019-09-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多