从UserControl的项目与自定义集合属性绑定

时间:2011-09-25 14:24:27

标签: wpf xaml data-binding

这个问题是this question的“续集”(我已经应用了答案,但它仍然不起作用)。

我正在尝试为模块化应用程序创建一个扩展的ToolBar控件,它可以从多个数据源加载它的项目(但这不是我现在想要解决的问题,现在我想让它工作时用作WPF中的常规ToolBar。

简而言之:我希望ToolBar的项能够绑定到tb:ToolBar的父级。

我有以下XAML代码:

<Window Name="myWindow" DataContext="{Binding ElementName=myWindow}" >
    <DockPanel>
        <tb:ToolBar Name="toolbar" DockPanel.Dock="Top" DataContext="{Binding ElementName=myWindow}>
            <tb:ToolBar.Items>
                <tb:ToolBarControl Priority="-3">
                    <tb:ToolBarControl.Content>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock>Maps:</TextBlock>
                            <ComboBox ItemsSource="{Binding SomeProperty, ElementName=myWindow}">

有关类型的一些信息:

  • tb:ToolBarUserControl,其依赖项属性Items的类型为FreezableCollection<ToolBarControl>

  • tb:ToolBarControlUserControl,其模板与ContentControl's template几乎完全相同。

问题是ComboBox中的绑定失败(使用通常的“无法找到与引用绑定的源”),因为其DataContext为null。

为什么吗

编辑:问题的核心是“为什么DataContext没有被继承?”,没有它,绑定就无法工作。

EDIT2:

以下是tb:ToolBar

的XAML
<UserControl ... Name="toolBarControl">
    <ToolBarTray>
        <ToolBar ItemsSource="{Binding Items, ElementName=toolBarControl}" Name="toolBar" ToolBarTray.IsLocked="True" VerticalAlignment="Top" Height="26">

编辑3:

我发布了一个有效和无效的示例:http://pastebin.com/Tyt1Xtvg

感谢您的回答。

4 个答案:

答案 0 :(得分:3)

我个人不喜欢在控件中设置DataContext的想法。我认为这样做会以某种方式破坏数据上下文继承。请看this post。我认为西蒙解释得很好。

至少,尝试删除

DataContext="{Binding ElementName=myWindow}"

<tb:ToolBar Name="toolbar" DockPanel.Dock="Top" DataContext="{Binding ElementName=myWindow}> 

看看它是怎么回事。

<强>更新

实际上,请保留所有现有代码(暂时忽略我之前的建议),只需更改

即可
<ComboBox ItemsSource="{Binding SomeProperty, ElementName=myWindow}"> 

<ComboBox ItemsSource="{Binding DataContext.SomeProperty}"> 

看看它是否有效。

我认为,由于您构建控件的方式,ComboBoxtb:ToolBarControltb:ToolBar处于同一级别/范围。这意味着它们共享相同的DataContext,因此您不需要任何ElementName绑定或RelativeSource绑定来尝试查找其父/祖先。

如果从DataContext="{Binding ElementName=myWindow}中删除tb:ToolBar,您甚至可以删除绑定中的前缀DataContext。这就是你真正需要的。

<ComboBox ItemsSource="{Binding SomeProperty}"> 

更新2 以回答修改3

这是因为Items用户控件中的tb:ToolBar集合只是一个属性。它不在逻辑和可视树中,我相信ElementName绑定使用逻辑树。

这就是它无法正常工作的原因。

添加到逻辑树

我想将Items添加到您需要做两件事的逻辑树中。

首先,您需要覆盖LogicalChildren用户控件中的tb:ToolBar

    protected override System.Collections.IEnumerator LogicalChildren
    {
        get
        {
            if (Items.Count == 0)
            {
                yield break;
            }

            foreach (var item in Items)
            {
                yield return item;
            }
        }
    }

然后,无论何时添加新的tb:ToolBarControl,您都需要致电

AddLogicalChild(item);

试一试。

这个工作......

稍微玩了一下之后,我想我上面给你看的是不够的。您还需要将这些ToolBarControls添加到主窗口的名称范围,以启用ElementName绑定。我假设您定义了Items依赖项属性。

public static DependencyProperty ItemsProperty =
    DependencyProperty.Register("Items",
                                typeof(ToolBarControlCollection),
                                typeof(ToolBar),
                                new FrameworkPropertyMetadata(new ToolBarControlCollection(), Callback));

在回调中,您可以将其添加到名称范围。

private static void Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var toolbar = (ToolBar)d;
    var items = toolbar.Items;

    foreach (var item in items)
    {
        // the panel that contains your ToolBar usercontrol, in the code that you provided it is a DockPanel
        var panel = (Panel)toolbar.Parent;
        // your main window
        var window = panel.Parent;
        // add this ToolBarControl to the main window's name scope
        NameScope.SetNameScope(item, NameScope.GetNameScope(window));

        // ** not needed if you only want ElementName binding **
        // this enables bubbling (navigating up) in the visual tree
        //toolbar.AddLogicalChild(item);
    }
}

此外,如果您想要属性继承,则需要

// ** not needed if you only want ElementName binding **
// this enables tunneling (navigating down) in the visual tree, e.g. property inheritance
//protected override System.Collections.IEnumerator LogicalChildren
//{
//    get
//    {
//        if (Items.Count == 0)
//        {
//            yield break;
//        }

//        foreach (var item in Items)
//        {
//            yield return item;
//        }
//    }
//}

我已经测试了代码并且工作正常。

答案 1 :(得分:2)

我把你发布的Xaml片段试图重现你的问题。

DataContext似乎从我能说的内容中继承得很好。但是,ElementName绑定失败,我认为这与以下事实有关:即使您在ComboBox中添加Window,它也会在不同的范围内结束。 (首先将其添加到自定义Items的{​​{1}}属性中,然后使用绑定填充到框架ToolBar

ToolBar绑定而不是RelativeSource绑定似乎工作正常。

但是如果你真的想在Binding中使用控件的名称,那么你可以查看Dr.WPF's excellent ObjectReference implementation

它看起来像这样

ElementName

我上传了一个小型示例项目,其中<Window ... tb:ObjectReference.Declaration="{tb:ObjectReference myWindow}"> <!--...--> <ComboBox ItemsSource="{Binding Path=SomeProperty, Source={tb:ObjectReference myWindow}}" RelativeSource在此处成功使用:https://www.dropbox.com/s/tx5vdqlm8mywgzw/ToolBarTest.zip?dl=0

我近似的自定义ObjectReference部分在ToolBar中看起来像这样 Window绑定失败,但ElementNameRelativeSource绑定工作

ObjectReference

答案 2 :(得分:1)

通常,如果没有DataContext,那么ElementName也将无效。如果情况允许,您可以尝试使用x:Reference

为此,您需要将绑定控件移动到引用控件的资源中,更改绑定并在其所在位置使用StaticResource,例如。

<Window Name="myWindow" DataContext="{Binding ElementName=myWindow}" >
    <Window.Resources>
        <ComboBox x:Key="cb"
                  ItemsSource="{Binding SomeProperty,
                                        Source={x:Reference myWindow}}"/>
    </Window.Resources>
    <DockPanel>
        <tb:ToolBar Name="toolbar" DockPanel.Dock="Top" DataContext="{Binding ElementName=myWindow}>
            <tb:ToolBar.Items>
                <tb:ToolBarControl Priority="-3">
                    <tb:ToolBarControl.Content>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock>Maps:</TextBlock>
                            <StaticResource ResourceKey="cb"/>

答案 3 :(得分:0)

正确的答案可能是将所有内容添加到逻辑树中,如前面的答案中所述,但以下代码已经证明对我来说很方便。我不能发布我的所有代码,但是......

编写自己的Binding MarkupExtension,它可以让您回到XAML文件的根元素。 此代码未编译,因为我修改了我的真实代码以发布此内容。

[MarkupExtensionReturnType(typeof(object))]
public class RootBindingExtension : MarkupExtension
{
    public string Path { get; set; }

    public RootElementBinding(string path)
    {
        Path = path;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        IRootObjectProvider rootObjectProvider =
            (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider));

        Binding binding = new Binding(this.Path);
        binding.Source = rootObjectProvider.RootObject;

        // Return raw binding if we are in a non-DP object, like a Style
        if (service.TargetObject is DependencyObject == false)
            return binding;

        // Otherwise, return what a normal binding would
        object providedValue = binding.ProvideValue(serviceProvider);

        return providedValue;
    }
}

用法:

<ComboBox ItemsSource={myBindings:RootBinding DataContext.SomeProperty} />