5

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.

Community
  • 1
  • 1
Matěj Zábský
  • 16,909
  • 15
  • 69
  • 114
  • tb:ToolBar and tb:ToolBarControl, are they custom control or usercontrol? – Justin XL Nov 23 '11 at 10:31
  • @Xin Both are UserControl (which I have also specified in the post). – Matěj Zábský Nov 23 '11 at 10:34
  • Yeah... I saw that, the reason that I asked is I wonder how you can make a usercontrol templated? – Justin XL Nov 23 '11 at 10:37
  • @Xin I created it originally as a simple class and only converted it to UserControl later (see the first thread I linked to in the post). The template is defined in Themes/generic.xaml (and I can pretty much turn it into CustomControl just by changing its ancestor, but I don't have any reason right now). The control itself works - it renders okay (the whole tb:ToolBar renders okay), just the bindings fail. – Matěj Zábský Nov 23 '11 at 10:54
  • Also I wonder why you need to do this and this – Justin XL Nov 23 '11 at 11:02
  • Did you at least try what I updated? – Justin XL Nov 24 '11 at 10:38
  • I just updated my answer for your Edit 3 – Justin XL Nov 24 '11 at 11:51
  • I have updated my answer, again, hope this time it works. :) – Justin XL Nov 28 '11 at 01:59
  • @Xin I will look at it, but I was a bit busy for last two days, so I didn't get to experiment on the visual tree stuff. Don't worry, I won't let the bounty expire :) – Matěj Zábský Nov 28 '11 at 07:51
  • No problem, it's not the bounty I'm worried about, I also want to get to the bottom of it, it's indeed a good question, ;) – Justin XL Nov 28 '11 at 08:50

4 Answers4

3

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.

Community
  • 1
  • 1
Justin XL
  • 38,763
  • 7
  • 88
  • 133
  • I removed the DataContext from the tb:ToolBar declaration and it didn't change anything (for better or worse), I guess it was just remnant of my earlier attempts to fix the problem. – Matěj Zábský Nov 23 '11 at 16:46
  • I've just updated the answer, please do a test and let me know. – Justin XL Nov 24 '11 at 01:07
  • @Xim Your UPDATE actually seems to work - what a simple fix! As for Update 2, do you know how could I make it a part of the tree (just like the original ToolBar does)? – Matěj Zábský Nov 24 '11 at 20:00
  • You probably need to change it to a custom control. You want something like a ListBox with ListBoxItems, in your case ToolBar and ToolBarControls. – Justin XL Nov 24 '11 at 20:51
  • @Xim I which sense would making it a CustomControl help me? AFAIK UserControl is mostly a Control with associated XAML file (and it is a Custom) and a Content property (inherited from ContentControl). – Matěj Zábský Nov 24 '11 at 21:20
  • Your ToolBar control is just a usercontrol right? If it inherits from usercontrol, how can you also make it inherited from ContentControl? You just can't mix them up like that. – Justin XL Nov 24 '11 at 21:30
  • @Xim UserControl inherits from ContentControl, most people (me included) rarely use the Content property though. – Matěj Zábský Nov 24 '11 at 22:19
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/5326/discussion-between-xin-and-matj-zabsky) – Justin XL Nov 24 '11 at 22:34
2

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>
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • Thanks for the reply (and example). However, I would like to see why WPF's ToolBar works and my tb:ToolBar doesn't, so I can fix it. Full sample here: http://pastebin.com/Tyt1Xtvg Of course, if nobody can answer that, I will accept your answer, because it appears to solve my immediate issue. – Matěj Zábský Nov 24 '11 at 10:28
1

Often if there is no DataContext then ElementName will not work either. One thing which you can try if the situation allows it is using x:Reference.

For that you need to move the bound control into the resources of the referenced control, change the binding and use StaticResource in the place where it was, e.g.

<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"/>
H.B.
  • 166,899
  • 29
  • 327
  • 400
0

The proper answer is probably to add everything to the logical tree as mentioned in previous answers, but the following code has proved to be convenient for me. I can't post all the code I have, but...

Write your own Binding MarkupExtension that gets you back to the root element of your XAML file. This code was not compiled as I hacked up my real code to post this.

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

Usage:

<ComboBox ItemsSource={myBindings:RootBinding DataContext.SomeProperty} />
JimSt24
  • 186
  • 5