ElementName绑定无法正常工作 - 怀疑调度程序竞争条件

时间:2012-09-21 15:47:43

标签: wpf data-binding itemscontrol dispatcher

Download repro project

修改

解决方案的工作有时会让我完全不知道为什么这首先是有效的。毕竟,这些项目不是Visual树的一部分。最后,它是完全有道理的:

  • 该集合中的按钮不在可视树中,因此元素绑定不起作用。
  • 应用模板将它们放入可视树和绑定中,如果此时应用,则开始工作。
  • 这确认了可疑的种族情况。

我的一位同事做了一些扩展调试,也显示了这个问题 - 在绑定成功的情况下,首先调用了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绑定 该视图无法可靠地工作。

enter image description here

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);

如果属性是从工作线程设置的(绑定引擎编组回到UI线程),那么ElementName绑定 有时 有效:

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;
});

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

这对我来说是一个很好的表演 - 任何指针都很受欢迎......

感谢您的建议 菲利普

3 个答案:

答案 0 :(得分:0)

据我所知,ElementName绑定在任何时候都不会更新:它只会绑定到属性一次然后停止更新。 这可以解释你的问题:第一个绑定将发生(或不会),具体取决于时间戳。

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

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

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

希望这有效=)

答案 1 :(得分:0)

我无法解释为什么第一个选项有效。但是我可以解释为什么其他的不会工作。

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

这样说你做:

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

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

现在说视觉树恰好是一个,因为绑定正在发生,然后绑定将成功,一切都很好。

请注意,在渲染内容时会混合到一个可视树中,例如: 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中,它实际上告诉&#34; YES,现在一切都呈现为一个可视树,你可以应用绑定。&#34;

如果你执行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.
                              });

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

答案 2 :(得分:0)

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

Fron 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;
            }
        }
    }
}