从自定义集合属性的UserControl项进行绑定

5
这个问题是这个问题的“续集”(我已经应用了答案,但它仍然无法工作)。
我正在尝试创建一个扩展的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:ToolBar 是一个具有依赖属性 Items,类型为 FreezableCollection<ToolBarControl>UserControl

  • tb:ToolBarControl 是一个具有与 ContentControl's template 几乎相同的模板的 UserControl

问题是在 ComboBox 中绑定失败(通常会出现“找不到引用的绑定源”),因为其 DataContext 为空。

为什么?

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

编辑2:

这里是 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

感谢您的回答。


tb:ToolBar和tb:ToolBarControl,它们是自定义控件还是用户控件? - Justin XL
是的...我看到了,我问的原因是我想知道如何将用户控件模板化? - Justin XL
@Xin 我最初创建它时只是一个简单的类,后来才转换为UserControl(请参见我在帖子中链接的第一个线程)。模板定义在Themes/generic.xaml中(我可以通过更改其祖先几乎将其转换为CustomControl,但我现在没有任何理由)。控件本身可以工作-它可以正常呈现(整个tb:ToolBar可以正常呈现),只是绑定失败了。 - Matěj Zábský
我刚刚更新了我的答案以回应你的第三次编辑。 - Justin XL
@Xin 我会看一下,但是过去的两天我有点忙,没有时间尝试可视化树形结构的东西。不用担心,我不会让赏金失效 :) - Matěj Zábský
显示剩余5条评论
4个回答

3
我个人不喜欢在控件中设置 DataContext 。我认为这样做会在某种程度上破坏数据上下文继承。请参考此帖子。我认为Simon解释得很好。
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绑定来查找其父/祖先。

如果从tb:ToolBar中删除DataContext =“{Binding ElementName=myWindow} ,甚至可以摆脱绑定中的前缀 DataContext 。而这确实是您所需的全部内容。

<ComboBox ItemsSource="{Binding SomeProperty}"> 

更新2,回答您的编辑3

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

这就是为什么它不起作用的原因。

将其添加到逻辑树

我认为要将Items添加到逻辑树中,您需要做两件事。

首先,您需要在tb:ToolBar自定义控件中覆盖LogicalChildren

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

我已经测试了代码,它可以正常工作。

我从tb:ToolBar声明中删除了DataContext,但它并没有改变任何东西(好或坏),我想这只是我早期尝试解决问题的残留物。 - Matěj Zábský
@Xim 你的更新似乎确实有效 - 多么简单的修复!至于更新2,你知道如何将它作为树的一部分(就像原始的ToolBar一样)吗? - Matěj Zábský
@Xim,把它作为CustomControl的方式可以如何帮助我?据我所知,UserControl大多是一个带有关联XAML文件(且是自定义的)和Content属性(继承自ContentControl)的控件。 - Matěj Zábský
你的ToolBar控件只是一个用户控件,对吧?如果它继承自用户控件,那么你怎么能让它同时继承自ContentControl呢?你不能这样混合使用它们。 - Justin XL
@Xim UserControl 继承自 ContentControl,但大多数人(包括我在内)很少使用 Content 属性。 - Matěj Zábský
显示剩余3条评论

2

我拿到了你发布的Xaml代码并尝试重现了你的问题。

据我所知,DataContext看起来是很好地继承了下来。然而,ElementName绑定失败,我认为这可能与你把ComboBox添加到Window中,但它最终处于不同的作用域有关。(它首先被添加到自定义ToolBarItems属性中,然后通过绑定填充到框架ToolBar中)

使用RelativeSource绑定而不是ElementName绑定似乎能够正常工作。

但如果你真的想在绑定中使用控件的名称,那么你可以查看Dr.WPF的出色的ObjectReference实现

它看起来会像这样:

<Window ...
        tb:ObjectReference.Declaration="{tb:ObjectReference myWindow}">
<!--...-->
<ComboBox ItemsSource="{Binding Path=SomeProperty,
                                Source={tb:ObjectReference myWindow}}"

我上传了一个小的示例项目,其中成功使用了RelativeSourceObjectReference。您可以在此处查看:https://www.dropbox.com/s/tx5vdqlm8mywgzw/ToolBarTest.zip?dl=0 在窗口中,自定义的ToolBar部分如下所示。
ElementName绑定失败,但RelativeSourceObjectReference绑定可行。
<Window ...
    Name="myWindow"
    tb:ObjectReference.Declaration="{tb:ObjectReference myWindow}">
<!--...-->
<tb:ToolBar x:Name="toolbar"
            DockPanel.Dock="Top"
            DataContext="{Binding ElementName=myWindow}">
    <tb:ToolBar.Items>
        <tb:ContentControlCollection>
            <ContentControl>
                <ContentControl.Content>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock>Maps:</TextBlock>
                        <ComboBox ItemsSource="{Binding Path=StringList,
                                                        ElementName=myWindow}"
                                    SelectedIndex="0"/>
                        <ComboBox ItemsSource="{Binding Path=StringList,
                                                        Source={tb:ObjectReference myWindow}}"
                                    SelectedIndex="0"/>
                        <ComboBox ItemsSource="{Binding Path=StringList,
                                                        RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                                    SelectedIndex="0"/>
                    </StackPanel>
                </ContentControl.Content>
            </ContentControl>
        </tb:ContentControlCollection>
    </tb:ToolBar.Items>
</tb:ToolBar>

谢谢回复(和示例)。但是,我想知道为什么 WPF 的 ToolBar 能够工作而我的 tb:ToolBar 却不能,这样我才能修复它。完整的示例在这里:http://pastebin.com/Tyt1Xtvg 当然,如果没有人能够回答,我会接受您的答案,因为它似乎解决了我的直接问题。 - Matěj Zábský

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"/>

0

正确的答案可能是将所有内容添加到逻辑树中,如先前的答案所提到的,但下面的代码对我来说已经被证明很方便了。我不能贴出我拥有的全部代码,但是...

编写自己的绑定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} />

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接