views:

992

answers:

5

I am binding some business objects to a WPF ItemsControl. They are displayed using a custom IValueConverter implementation used to produce the Geometry for a Path object in the DataTemplate as shown here:

<ItemsControl x:Name="Display" 
              Background="White"
              HorizontalAlignment="Stretch" 
              VerticalAlignment="Stretch"
              ItemsSource="{Binding ElementName=ViewPlaneSelector, 
                                    Path=SelectedItem.VisibleElements}" 
           >
    <ItemsControl.Resources>
        <!-- This object is just used to get around the fact that ConverterParameter 
        can't be a binding directly (it's not a DependencyProperty on a DependencyObject -->
        <this:GeometryConverterData 
            x:Key="ConverterParameter2"
            Plane="{Binding ElementName=ViewPlaneSelector, 
                            Path=SelectedItem}" />
        <DataTemplate DataType="{x:Type o:SlenderMember}">
            <Path Stroke="Blue" StrokeThickness=".5"
              Data='{Binding Converter={StaticResource SlenderMemberConverter}, 
                            ConverterParameter={StaticResource ConverterParameter2}}'
              ToolTip="{Binding AsString}">
            </Path>
        </DataTemplate>
    </ItemsControl.Resources>
</ItemsControl>

Note that the items for the ItemsControl are drawn from the ViewPlaneSelector (a ComboBox) SelectedItem.VisibleElements property. I need that same ViewPlaneSelector.SelectedItem in the SlenderMemberConverter to figure out how to display this element. I'm trying to get a reference to it into the converter by creating the intermediate GeometryConverterData object in the Resources section. This object exists solely to get around the problem of not being able to bind directly to the ConverterParameter property (as mentioned in the comments). Here is the code for the GeometryDataConverter class:

class GeometryConverterData : FrameworkElement {
    public static readonly DependencyProperty PlaneProperty =
        DependencyProperty.Register("Plane", typeof(ViewPlane), 
            typeof(GeometryConverterData), null, ValidValue);

    public static bool ValidValue(object o){
        return true;
    }

    public ViewPlane Plane {
        get{
            return GetValue(PlaneProperty) as ViewPlane;
        }set{
            SetValue(PlaneProperty, value);
        }
    }
}

I added the ValidValue function for debugging, to see what this property was getting bound it. It only and always gets set to null. I know that the ViewPlaneSelector.SelectedItem isn't always null since the ItemsControl has items, and it's items are drawn from the same property on the same object... so what gives? How can I get a reference to this ComboBox into my ValueConverter.

Or, alternately, why is what I'm doing silly and overly complicated. I'm as guilty as many of sometimes getting it into my head that something has to be done a certain way and then killing myself to make it happen when there's a much cleaner and simpler solution.

A: 

You can always watch for binding errors in Output window. It always starts with System.Windows.DataError ...

Trainee4Life
Sorry I guess the question was unclear... that part works perfectly. It retrieves the VisibleElements collection and displays them. The part that doesn't work is: Plane="{Binding ElementName=ViewPlaneSelector,Path=SelectedItem}" /> In the GeometryConverterData instantiation (it's always null)
MKing
Are you getting any errors in the Output window? Another guess would be that since the <this:GeometryConverterData /> object is never part of the WPF visual tree, the binding could never find any element with the name "ViewPlaneSelector". That is, this binding will come only in effect when you have actually used this resource somewhere in the visual tree as a node.
Trainee4Life
There are no errors in the Output window, no. It appears that everything is working as designed it's just my understanding of correct behavior that is lacking. You may be correct about the visual tree, however. As an experiment I changed the ElementName to some gibberish and it behaved exactly as it does now (supplies a null binding value). I'll research.
MKing
A: 

I'm not very sure, but have you tried using a DynamicResource in your ConverterParameter, as in

        <Path Stroke="Blue" StrokeThickness=".5"
          Data='{Binding Converter={StaticResource SlenderMemberConverter}, 
                        ConverterParameter={DynamicResource ConverterParameter2}}'
          ToolTip="{Binding AsString}">
        </Path>

?

luvieere
Yes, unfortunately you can only use DynamicResource to bind to a DependencyProperty on a DependencyObject (here's the error): A 'DynamicResourceExtension' cannot be set on the 'ConverterParameter' property of type 'Binding'. A 'DynamicResourceExtension' can only be set on a DependencyProperty of a DependencyObject.
MKing
A: 

Where exactly do you intend to use your DataTemplate? I can see your mistake, as well as a more clean way to do it, but it is kind of hard to explain without the full picture.

Fyodor Soikin
The DataTemplate is for the ItemsControl where it is placed... SlenderMember type elements are bound to the ItemsControl and are displayed according to the DataTemplate
MKing
But it's not referenced anywhere.Did you put it inside the <Resources> tag by mistake?
Fyodor Soikin
And then another thing I don't get: your DataTemplate does not depend on the ItemsControl's current item. Therefore, assuming what you're trying to do worked, it would always show path for the same object - the one selected in the ViewPlaneSelector combo box.I assume it was your intention to have ItemsControl.SelectedItem as an argument for your converter instead of ComboBox.SelectedItem, wasn't it?
Fyodor Soikin
Ah, sorry, I take my latest comment back.So it seems to me that your converter converts an item of VisibleElements collection into geometry for path, but it also needs to know its "owner" element - the one selected in the combo box, correct?
Fyodor Soikin
You got it. It doesn't need to be referenced because it's keyed by DataType and your second post is absolutely correct. It takes from that collection and then uses the source element (from the ComboBox) to determine it's appearance relative to that element. For reference, the ComboBox contains multiple different views which could show some of the same objects but from different perspectives. So an object (SlenderMember) can be in multiple views and multiple views can contain the same object.
MKing
For what it's worth, this worked perfectly when there was only one ComboBox selector, I just retrieved it by name from the main window. The problem is I want to have a number of different view panels each with their own drop downs and view of the element set.
MKing
+1  A: 

You can't. Just give up...

No-no. Wait :). Use MultiBinding instead of binding. At least it looks like you are trying to do something that it let's you to do.

Hope this helps.

Anvaka
That's a really great idea. Unfortunately it doesn't seem like you can target an element by name from within a DataTemplate. The Binding using ElementName always returns DependencyProperty.UnsetValue. Your first response might have been dead on! There's got to be a way to achieve what I want, I suspect I need to take like five steps back and come from another direction. In the end I just want my DataTemplate to be dependent on another element, can't see why this is hard.
MKing
A: 

Sorry, but you're doing it wrong.

Your entire forum post is one code smell after another.
1. You're using Value Converters to convert a business entity to it's visual counterpart. That's a code smell in my book.
2. You're looking into sending a UI control to business logic.
3. Honest to god, someone here said "MultiBindings" (and beyond being right) my head imploded and there is now a black hole of nothingness where my brain used to be.

For more on my religious convictions regarding Value Converters, Trigger, MultiBindings and other worst practices for real-world LOB development - feel free to read the following thread on WPF Disciples.

However, Let's focus on getting you untangled. You need a - ViewModel.

Here's what you need to put inside that ViewModel:
1. Controls don't bind to each other. It's an abomination unseen since biblical times. Whenever somthing on a control changes (ComboBox.SelectedItem, TextBox.Text, whatever) bind that directly to a ViewModel. So, instead of binding to PlaneSelector.SelectedItem and PlaneSelector.SelectedItem.VisibleElements, bind the selectedItem back to the ViewModel.
2. Convert between your business data and your correponding visual representation in the ViewModel. So, have the view model return the geometric data.

Here's a rough draft of how a super simple ViewModel that does that look like:

public class myFormViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void InvokePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler changed = PropertyChanged;
        if (changed != null) changed(this, new PropertyChangedEventArgs(propertyName));
    }


    private object _planeSelectedItem;
    public object PlaneSelectedItem
    {
        get { return _planeSelectedItem; }
        set
        {
            _planeSelectedItem = value;
            InvokePropertyChanged("PlaneSelectedItem");
        }
    }

    public IEnumerable<KeyValuePair<string, Geometry>> VisibleElements
    {
        get
        { 
           foreach(var slenderMember in PlaneSelectedItem.VisibleElements)
            {
                yield return new KeyValuePair<string, Geometry>(slenderMember.AsString, ToGeometry(slenderMember));
            }
        }
    }
}

And here's roughly how that part of your control should look like:

<ComboBox SelectedItem="{Binding PlaneSelectedItem, Mode=TwoWay}" />
<ItemsControl x:Name="Display" 
      Background="White"
      HorizontalAlignment="Stretch" 
      VerticalAlignment="Stretch"
      ItemsSource="{Binding VisibleElements}">
    <ItemsControl.ItemTemplate>
        <DataTemplate >
            <Path Stroke="Blue" StrokeThickness=".5"
                  Data="{Binding Value}" ToolTip="{Binding Key}">
            </Path>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Obviously, there's some missing code here (Setting the this.DataContent = new myFormViewModel(), using KeyValuePairs, setting the ComboBox ItemsSource, etc). But the core here is to simplify these crazy binding shenanigans.

Instead of trying to hack togather XAML to behave like code - just use code. It'll make your life much simpler.

JustinAngel