views:

352

answers:

2

I'm in the process of making a custom control -- specifically a pie chart. I want the markup to look something like this example:

<c:PieChart>
    <!-- These dependency properties are never set -->
    <c:Slice Value="{Binding RedCount}" />
    <c:Slice Value="{Binding BlueCount}" />
    <c:Slice Value="{Binding GreenCount}" />
</c:PieChart>

PieChart derives from Control:

[ContentProperty("Slices")]
public class PieChart : Control
{
    public PieChart()
    {
        Slices = new ObservableCollection<Slice>();
    }

    public ObservableCollection<Slice> Slices { get; private set; }
}

The above XAML causes the Slices property to be populated with three instances of Slice.

Here's a snippet from Slice:

public class Slice : ContentControl
{
    public static DependencyProperty ValueProperty
        = DependencyProperty.Register(
            "Value", typeof(double), typeof(Slice),
            new PropertyMetadata((p,a) => ((Slice)p).OnValueChanged(a)));

    private void OnValueChanged(DependencyPropertyChangedEventArgs e)
    {
        // This code never called!
    }
}

The problem is that the Value property on Slice is never set. If I put the dependency property on the PieChart and copy the binding expression, then the value is set, so I'm confident that I understand the basics of dependency properties and bindings.

So, what do I have to do to have my dependency property set a value on the child item? As these items are in my own collection, are they somehow not in the logical tree recognised by whatever magic makes bindings work?

A simple 'do this' answer would be helpful, but I'd really like to understand what's going wrong here, so I'll accept the most insightful, explanatory answer posted.

EDIT I didn't make it clear but there is some ambient object on the DataContext of the PieChart that contains the properties RedCount, etc. In the past, when I've had a typo in a binding's path (or some other mistake) I've seen warnings in the VS output window at runtime. In this case, I see nothing.

A: 

I think the problem you're seeing is that the bindings are trying to use the wrong source. I suspect something like this will work for you:

<c:PieChart>
    <!-- These dependency properties are never set -->
    <c:Slice Value="{Binding RedCount, RelativeSource={AncestorType c:PieChart}}" />
    <c:Slice Value="{Binding BlueCount, RelativeSource={AncestorType c:PieChart}}" />
    <c:Slice Value="{Binding GreenCount, RelativeSource={AncestorType c:PieChart}}" />
</c:PieChart>

I think that's close, but I might be way off. I can't really look it up right now. But it sounds good to me. (Sorry if it's completely wrong :) )

MojoFilter
After reading the question again, I don't think those values are coming from the PieChart class in the first place... which makes this completely pointless. You're welcome.
MojoFilter
Picky point -- I think that should be {Binding RelativeSource={RelativeSource AncestorType...}} (i.e. you need to repeat RelativeSource inside the RelativeSource expression). Yeah, annoying I know.
itowlson
Thanks but no, the properties are defined on some ambient data context object. If the bindings were failing, I'd see warning messages in the VS output window, but none are displayed. I'll edit the post to mention this.
Drew Noakes
+2  A: 

It looks like the Slice controls aren't getting hooked up to the visual tree. They are just member variables, and WPF doesn't know it's meant to be displaying them as child elements of the PieChart. A couple of possible approaches:

Have PieChart call AddLogicalChild to add the Slice objects. You will need to handle the possibility of Slices being added to or removed from the collection at runtime. This is pretty low level: MSDN advises, "For control authors, manipulating the logical tree at this level is not the recommended practice, unless none of the content models for available base control classes are appropriate for your control scenario. Consider subclassing at the level of ContentControl, ItemsControl, and HeaderedItemsControl."

Make PieChart a Panel. The Slices will then be its children, but note that the Children property is weakly typed, so people could add anything to a PieChart. This is not inherently a bad or a good thing; it's a design consideration (see note below). But it's probably not what you want in your case.

Make PieChart an ItemsControl, and override GetContainerForItemOverride, IsItemItsOwnContainerOverride and possibly PrepareContainerForItemOverride to create/respect Slice controls. (Cf. ListBox and ListBoxItem, ComboBox and ComboBoxItem, etc.) A nice feature of this is that users can then use an ItemsSource and a DataTemplate (like a ListBox) instead of having to create the child controls by hand. (But users can still create fixed Slices in XAML per your syntax, just as they can create ListBoxItems in XAML.)

You could even consider a hybrid approach, e.g. factoring out the radial layout functionality into a RadialLayoutPanel and the pie slice generation into an ItemsControl which uses a RadialLayoutPanel as its ItemsPanel.

itowlson