views:

1666

answers:

2

Hello,

I'm working on a ListBox that overrides its' ItemsPanelTemplate to use a Canvas instead of a StackPanel. ListBoxItems have a DataTemplate that uses a converter to define the look and position of each ListBoxItem on the canvas. When I add an item to the collection that the ListBox is bound to, I'd like to be able to add other UIElements to the canvas. Can I accomplish this outside of the ListBoxItem's converter?

my resources section is like this:

    <Style TargetType="ListBox">
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <Canvas x:Key="cnvAwesome">

                    </Canvas>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="ListBoxItem">
        <Setter Property="Canvas.Left" Value="{Binding Path=Position.X}" />
        <Setter Property="Canvas.Top" Value="{Binding Path=Position.Y}" />
    </Style>

And a ListBox:

    <ListBox x:Name="lstAwesome" ItemsSource="{Binding Source={StaticResource someCollection}}"/>

And the collection that the ListBox is bound to:

public ObservableCollection<SomeType> someCollection {get; set; }

So, if I have a method to add an item to the collection someCollection that the ListBox lstAwesome is bound to:

private void AddItemToDataboundCollection(SomeType someItem)
{
    someCollection.Add(someItem)
    // I'd like to add a UIElement to the canvas that the ListBox uses to layout items here.
}

So, can I add UIElements to a canvas being used for an ItemsPanel by a databound listbox? Programmatically, when I'm adding items to the collection that that ListBox is bound to?

I'd greatly appreciate any input!

+1  A: 

You specify the way your items to be displayed by setting an ItemTemplate, like:

<ListBox.ItemTemplate>
    <DataTemplate>
        <Rectangle Width="{Binding Width}" Height="{Binding Height}" />
    </DataTemplate>
</ListBox.ItemTemplate>

If you want your ItemTemplate to be changed based on your item then you can set an ItemTemplateSelector and decide which ItemTemplate to be used programmatically.

idursun
I dont' think this answer addresses my question. If a ListBox is using a canvas for its' ItemsPanel, how can I programatically add other UIElements to that panel?
Then please give a concrete example about what you really want to achieve.
idursun
I edited my original post for clarity. Please let me know if anything is unclear. Thanks idursun!
@barriovolvo, I think you cannot do what you want in a single listbox, maybe overlaying listboxes can help, or you can check adorners in WPF.
idursun
+2  A: 

I think that you have everything in place in your question to add items to the Canvas of your ListBox. I just put together a simple WPF test app using the code in your question, and it just worked. Whenever I added items to the ObservableCollection that is bound to the ItemsSource of the ListBox, WPF would create a ListBoxItem for that object, and based on the style of the ListBoxItem, the item would be drawn at the correct location by the Canvas.

Were you not able to get your code above working? If that is the case, what error did you get, or what did not work as you expected it to?

Edit: After reading the question again, I think that the confusion is due to a misunderstanding of how WPF binding works. When you bind a collection to a ListBox's ItemsSource, all that you need to do is add items to that collection (assuming that the collection implements INotifyCollectionChanged, which ObservableCollection does). WPF subscribes to the events provided by that interface, and creates/destroys ListBoxItems based on the changes made to the list.

Edit 2: While it would be better to use a DataTemplate for this, so that the line will automatically be added when an item is added to the collection, I'm not sure how you could get the position of the previously added item so that the line would start at the right location.

One option that could work would be to subclass Canvas, and set that as the ItemsPanelTemplate for the ListBox. I believe that it has a function that you can override which gets called whenever a control is added to it. In that function, you could get the position of the previously added control (which would be cached in your Canvas subclass), and then add to line control to the controls of the Canvas directly. This does assume that the position of controls in the Canvas can't change. If they can, you'll probably have to subscribe to the object's PropertyChanged event and update the lines accordingly.

Edit 3: Since binding doesn't seem like an option due to your requirements, the least bad option is to just add the children directly. You can do that by searching through the visual tree using VisualTreeHelper.GetChild():

private T FindChild<T>(FrameworkElement parent, string name)
    where T : FrameworkElement
{
    FrameworkElement curObject = null;
    T obj = default(T);
    int count = VisualTreeHelper.GetChildrenCount(parent);
    for(int i = 0; i < count; i++)
    {
        curObject = VisualTreeHelper.GetChild(parent, i) as FrameworkElement;
        obj = curObject as T;
        if(null != obj && obj.Name.Equals(name))
        {
            break;
        }

        obj = FindChild<T>(curObject, name);
        if(null != obj)
        {
            break;
        }
    }

    return obj;
}

Use it like this:

Canvas canvas = FindChild<Canvas>(lstAwesome, "cnvAwesome");

This code recursively searches through the visual tree of parent for a control of the specified type and name. It would be better to use DependencyObject over FrameworkElement here (since that is what is returned by VisualTreeHelper.GetChild()), however Name is only defined on FrameworkElement.

If you want to get fancy, you can even make this an extension method.

Andy
Hey Andy, thanks for checking this out. And, yes, items display correctly for me too. What I am asking is if I can add additional UIElements to the canvas, *in addition* to the ListItems. I don't seem to be able to obtain a reference to cnvAwesome (see xaml)
Would it work to modify the ItemsPanelTemplate to add the items to the Canvas there? Otherwise, if you want to add one additional control per ListBoxItem, I'd suggest following idursun's approach and modify the DataTemplate so that the additional controls will automatically be added whenever you add an item to the collection.
Andy
I'd like to follow idursun's example, but these UIElements would need to be added procedurally, at runtime. Specifically, when an item gets added to the collection that the ListBox is bound to. I just can't get a handle on the ItemsPanel that the ListBox uses, and am unable to add UIElements to it. I really do appreciate the help, and please let me know if I can make my problem any clearer.
What exactly do you want to add to the Canvas when you add an item to the collection? If you give some specifics, I might be able to help you do it using an ItemTemplate instead of having to manually add the control to the Canvas yourself.
Andy
I'm binding against a collection of custom objects, which have a Property "Position", of type Point. A converter uses this property to set each ListBoxItem's Canvas.Top and Canvas.Left, and an ItemTemplate describes the appearance of each item. I'd like to add a Line to the Canvas when an item is added to the collection, connecting the ListBoxItem to another one.
Hi again, and thanks for the suggestions. Unfortunately, the line doesn't connect to the previously added item necessarily, but to another arbitrarily determined item. The result would look something like an undirected network graph. And, yes, the positions of items (and therefore line end-points) will change while the program is running. I think this means that adorners are out. If I could just get a reference to the canvas that the listbox uses (cnvAwesome in the original example), so I could add UIElements (lines) to its' Children collection, life would be great. Is it possible?
Great, this works! I just wanted to thank you for the help! idurson too.
p.s. Awesome! :D
Also, that should be "GetChildrenCount", not "GetChildCount". Not nit-picking; this is for posterity. :)
I updated the code sample to use the right method name. Thanks for pointing that out. :)
Andy
Also, the line where this method calls itself only passes in a single parameter, a FrameworkElement, but the signature also calls for a string. Changing the recursive call to obj = FindChild(curObject, name);Gives a compiler error of "The type arguments for method 'myAwesomeClass.FindChild<T>(System.Windows.FrameworkElement, string)' cannot be inferred from the usage. Try specifying the type arguments explicitly."
@barriovolvo - thanks for pointing that out, it should be fixed now.
Andy
@Andy - Thanks! But the line that makes the recursive call still gives me that "Type cannot be inferred from the usage" error. Any idea why?
You probably need to specify the generic parameter to the FindChild() call (I modified the code sample to include this). Sorry for all the little mistakes - I guess I should've ran this code through a compiler before posting it.
Andy