Reputation:
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 UIElement
s 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
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
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