1

下载复制项目

编辑

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

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

我的一位同事进行了一些扩展调试,也显示了该问题 - 在绑定成功的情况下,首先调用 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,这总是会导致绑定中断,而没有睡眠,它只是有时会中断。

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

谢谢你的建议菲利普

4

3 回答 3

0

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

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

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

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

希望这有效=)

于 2012-09-21T17:56:57.463 回答
0

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

好的,首先,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.
                              });

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

于 2012-09-21T18:49:36.670 回答
0

鉴于 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;
            }
        }
    }
}
于 2012-09-22T14:35:10.127 回答