Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding from items of an UserControl with custom collection property

This question is a "sequel" to this question (I have applied the answer, but it still won't work).

I'm trying to create an extended ToolBar control for a modular application, which can load its items from multiple data sources (but that is not the issue I'm trying to solve right now, now I want it to work when used as regular ToolBar found in WPF).

In short: I want the ToolBar's items to be able to bind to the tb:ToolBar's parents.

I have following XAML code:

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

Some info about the types:

  • tb:ToolBar is an UserControl with dependency property Items of type FreezableCollection<ToolBarControl>.

  • tb:ToolBarControl is an UserControl with template pretty much identical to ContentControl's template.

The problem is that the binding in the ComboBox fails (with the usual "Cannot find source for binding with reference"), because its DataContext is null.

Why?

EDIT: The core of the question is "Why is the DataContext not inherited?", without it, the bindings can't work.

EDIT2:

Here is XAML for the tb:ToolBar:

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

EDIT 3:

I posted an example of what works and what doesn't: http://pastebin.com/Tyt1Xtvg

Thanks for your answers.

like image 876
Matěj Zábský Avatar asked Sep 25 '11 14:09

Matěj Zábský


2 Answers

I personally don't like the idea of setting DataContext in controls. I think doing this will somehow break the data context inheritance. Please take a look at this post. I think Simon explained it pretty well.

At least, try removing

DataContext="{Binding ElementName=myWindow}"

from

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

and see how it goes.

UPDATE

Actually, keep all your existing code (ignore my previous suggestion for a moment), just change

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

to

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

and see if it works.

I think because of the way you structure your controls, the ComboBox is at the same level/scope as the tb:ToolBarControl and the tb:ToolBar. That means they all share the same DataContext, so you don't really need any ElementName binding or RelativeSource binding to try to find its parent/ancestor.

If you remove DataContext="{Binding ElementName=myWindow} from the tb:ToolBar, you can even get rid of the prefix DataContext in the binding. And this is really all you need.

<ComboBox ItemsSource="{Binding SomeProperty}"> 

UPDATE 2 to answer your Edit 3

This is because your Items collection in your tb:ToolBar usercontrol is just a property. It's not in the logical and visual tree, and I believe ElementName binding uses logical tree.

That's why it is not working.

Add to logical tree

I think to add the Items into the logical tree you need to do two things.

First you need to override the LogicalChildren in your tb:ToolBar usercontrol.

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

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

Then whenever you added a new tb:ToolBarControl you need to call

AddLogicalChild(item);

Give it a shot.

This WORKS...

After playing around with it a little bit, I think what I showed you above isn't enough. You will also need to add these ToolBarControls to your main window's name scope to enable ElementName binding. I assume this is how you defined your Items dependency property.

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

In the callback, it is where you add it to the name scope.

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

Also if you want property inheritance, you will need

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

I have tested the code and it works fine.

like image 198
Justin XL Avatar answered Sep 29 '22 04:09

Justin XL


I took the pieces of Xaml that you posted and tried to reproduce your problem.

The DataContext seems to be inheriting just fine from what I can tell. However, ElementName Bindings fail and I think this has to do with the fact that even though you add the ComboBox in the Window, it ends up in a different scope. (It is first added to the Items property of the custom ToolBar and is then populated to the framework ToolBar with a Binding)

A RelativeSource Binding instead of an ElementName Binding seems to be working fine.

But if you really want to use the name of the control in the Binding, then you could check out Dr.WPF's excellent ObjectReference implementation

It would look something like this

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

I uploaded a small sample project where both RelativeSource and ObjectReference are succesfully used here: https://www.dropbox.com/s/tx5vdqlm8mywgzw/ToolBarTest.zip?dl=0

The custom ToolBar part as I approximated it looks like this in the Window.
ElementName Binding fails but RelativeSource and ObjectReference Bindings work

<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>
like image 34
Fredrik Hedblad Avatar answered Sep 29 '22 06:09

Fredrik Hedblad