barriovolvo
barriovolvo

Reputation:

WPF: adding UIElements to a ListBox whose ItemsPanelTemplate is a canvas?

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!

Upvotes: 1

Views: 3880

Answers (2)

Andy
Andy

Reputation: 30418

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.

Upvotes: 2

idursun
idursun

Reputation: 6335

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.

Upvotes: 1

Related Questions