How do I get the child controls of a ListBox? - c#

I have a ListBox in xaml which has a sub-ListBox inside the top-level ListBox's item template. Because the sub-ListBox is multi-select, and I can't bind the SelectedItems of the sub-ListBox to a viewmodel property for some reason, I'm trying to do a lot of this in the view code-behind.
I have everything working, except for one snag: I want to select all items in each sub-ListBox by default. Since the SelectedItems aren't data-bound, I'm trying to do it manually in code whenever a SelectionChanged event fires on the top-level ListBox. The problem is that I don't know how to get from the top-level ListBox to the sub-ListBox of the top-level selected item. I think I need to use the visual tree, but I don't know how to even get the dependency object that corresponds to the selected item.
Here's the code:
<ListBox ItemsSource="{Binding Path=Stuff}" SelectionChanged="StuffListBox_SelectionChanged" SelectedItem="{Binding Path=SelectedStuff, Mode=TwoWay}" telerik:RadDockPanel.Dock="Bottom">
<ListBox.ItemTemplate>
<DataTemplate>
<telerik:RadDockPanel>
<TextBlock Text="{Binding Path=Name}" telerik:RadDockPanel.Dock="Top" />
<ListBox ItemsSource="{Binding Path=SubStuff}" SelectionMode="Multiple" SelectionChanged="SubStuffListBox_SelectionChanged" Visibility="{Binding Converter={StaticResource StuffToSubStuffVisibilityConverter}}" telerik:RadDockPanel.Dock="Bottom">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</telerik:RadDockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The converter ensures that only the selected top-level item has a visible sub-ListBox, and that's working.
I need to implement the following method:
private void StuffListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox stuffListBox = (ListBox)sender;
foreach (Stuff stuff in e.AddedItems)
{
...
subStuffListBox.SelectAll();
}
}
I tried doing stuffListBox.ItemContainerGenerator.ContainerFromItem(stuff), but that always returns null. Even stuffListBox.ItemContainerGenerator.ContainerFromIndex(0) always returns null.
I also get strange behaviour from the selection changed method. 'e.AddedItems will contain items, but stuffListBox.SelectedItem is always null. Am I missing something?
From what I've read, my problem is coming from the fact that the containers haven't been generated at the time that I'm getting a selection change event. I've seen workarounds that involve listening for a the item container generator's status changed event, but I'm working in Silverlight and don't have access to that event. Is what I'm doing just not possible in Silverlight due to the oversight of making SelectedItems on a ListBox read-only?

Like you say this is probably best done in the ViewModel but you can select all the sub list items in code behind using VisualTreeHelper.
private void StuffListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var stuffListBox = (ListBox)sender;
ListBoxItem item = (ListBoxItem)stuffListBox.ContainerFromItem(stuffListBox.SelectedItem);
ListBox sublist = FindVisualChild<ListBox>(item);
sublist.SelectAll();
}
FindVisualChild Method as per MSDN
private childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
return (childItem)child;
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}

Related

How to set focus to a bound ListboxItem by pressing its child element?

I am developping a small WPF application which consist mostly in displaying ObservableCollection<> in others ObservableCollection<>, and so on.
Here is a code example of what my application looks like:
<Listbox Name="MainList" ItemsSource={Binding}>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Textblock Text={Binding MainName} />
<Button>Add item</Button>
<Button>Delete item</Button>
<Listbox Name="ChildList" ItemsSource="{Binding Path=ChildItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<Textblock Text={Binding ChildName} />
</DataTemplate>
</ListBox.ItemTemplate>
</Listbox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</Listbox>
And visually it pretty much looks like this:
EDIT:
I will re-explain what I am trying to do.
Whenever I click Button A or Button B I want to Select the MainList ListBoxItem in which they are contained (i.e: A Item)
And in a second time whenever I click Button B:
I want to be sure that a ListBoxItem is selected in ChildList(Second Listbox in the picture)
And if so, I want to delete it in code-behind.
But my main problem is since everything is generated by my bindings I cannot get, so far, an element from my ChildList because ChildList is duplicated in any of my MainList ListBoxItem.
If I understand well the problem is that you want first click on a button of unselected item to select the MainItem, and on next click, when MainItem is already selected, preform click action. Try this when button is clicked:
private ListBoxItem FindItemContainer(DependencyObject obj)
{
while (obj != null && !(obj is ListBoxItem))
{
obj = VisualTreeHelper.GetParent(obj);
}
if (obj != null)
return obj as ListBoxItem;
else
return null;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var lbi = FindItemContainer(sender as DependencyObject);
if (lbi != null)
{
if (lbi.IsSelected)
{
//do click event
}
else
lbi.IsSelected = true;
}
}
Of course you can also do it more MVVM way by binding ListBoxItem.IsSelected to lets say bool MainItem.MyItemIsSelected
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Path=MyItemIsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
and Button.Command to your ICommand MainItem.DeleteCommand and then when command is executed do something like that:
if (MyItemIsSelected)
{
//do command body
}
else
MyItemIsSelected = true;
which will be better long term because you could replicate SelectedItem behaviour in ChildList object (add MyItemIsSelected and bind it to inner 'ListBoxItem.IsSelected, like discribed above) and add MySelectedItem property to ChildList:
ChildItem MySelectedItem
{
get
{
return Items.FirstOrDefault(n=>n.MyItemIsSelected);
}
}
and your delete command would look like this:
if (MyItemIsSelected)
{
ChildItem selItem = ChildItems.MySelectedItem;
if (selItem != null) ChildItems.Items.Remove(selItem);
}
else
MyItemIsSelected = true;
if everything is data bound and lists are ObservableCollections then you can do all that in object and UI will follow. Actually you can do only this child selection binding bit and still use first solution and in Button_Click look like this:
private void Button_Click(object sender, RoutedEventArgs e)
{
var lbi = FindItemContainer(sender as DependencyObject);
if (lbi != null)
{
if (lbi.IsSelected)
{
MainItem mainItem = lbi.Content as MainItem;
ChildItem selChild = mainItem.ChildItems.MySelectedItem;
if (selChild != null) mainItem.ChildItems.Items.Remove(selChild);
}
else
lbi.IsSelected = true;
}
}
Here is simple, working example on Dropbox
You can do everything you want to do in code behind:
Find the item on which the Button is pressed: in the click-event, cast the sender parameter to type Button. Its DataContext property will contain the item you want to select.
Select the item: set MainList.SelectedItem to the item.
Focus will be on the Button, but that should be ok, since it is inside the item.
Find the selected item in second listbox: locating the ListBox in the DataTemplate is tricky, but you could set its IsSynchronizedWithCurrentItem property to True, and then use the underlying child collection's default CollectionView. You'd find the current item of MainList like above. Then you'd use:
itemToDelete = CollectionViewSource.GetDefaultView(item.ChildItems).CurrentItem;
item.ChildItems.Remove(itemToDelete);

LogicalTreeHelper.GetChildren - ObservableCollection Move() causing ContentControl in DataTemplate to lose it's content?

This is very strange. The point of the code below is to support an attachedProperty which will notify a container if any of it's children have received focus.
i.e. I have a Grid with a textBox somewhere in it's Content and I want to turn the Grid Blue if one of those controls gets focus.
I have a ListView with an ItemsTemplate. The ItemsTemplate is a DataTemplate containing a few things...but one of them being a ContentControl.
Example:
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Border>
<ContentControl Content="{Binding Something}"/>
</Border>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The Binding on the ContentControl should display a certain type of UserControl.
Upon creation...works just fine. If I recursively iterate template of the listViewItem starting with the Grid element...it will traverse the ContentControl's "Content" as well.
HOWEVER...once I do a .Move() on the ObservableCollection that the ListView ItemsSource is bound to, the ContentControl.Content is empty according to the LogicalTreeHelper.
What gives?
If I inspect the ContentControl, it SHOWS me the content...but LogicalTreeHelper.GetChildren returns and empty Enumerator.
I'm confused...
Can anyone explain why this would be the case?
LogicalTreeHelper iterator method
public static void applyFocusNotificationToChildren(DependencyObject parent)
{
var children = LogicalTreeHelper.GetChildren(parent);
foreach (var child in children)
{
var frameworkElement = child as FrameworkElement;
if (frameworkElement == null)
continue;
Type frameworkType = frameworkElement.GetType();
if (frameworkType == typeof(TextBox) || frameworkType == typeof(ListView) ||
frameworkType == typeof(ListBox) || frameworkType == typeof(ItemsControl) ||
frameworkType == typeof(ComboBox) || frameworkType == typeof(CheckBox))
{
frameworkElement.GotFocus -= frameworkElement_GotFocus;
frameworkElement.GotFocus += frameworkElement_GotFocus;
frameworkElement.LostFocus -= frameworkElement_LostFocus;
frameworkElement.LostFocus += frameworkElement_LostFocus;
// If the child's name is set for search
}
applyFocusNotificationToChildren(child as DependencyObject);
}
}
Aloha,
Here is an suggetion how you could solve your problem:
I am not sure if i spelled right the GotFocus event but its a RoutedEvent and you can use it anywhere in your visual tree.
If one of your items receives a focus your ListView will get notified and inside the handler you can do whatever you want.
How about this:
<ListView GotFocus="OnGotFocus">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Border>
<ContentControl Content="{Binding Something}"/>
</Border>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This is just some random logic to demonstrate what you can do.
public void OnGotFocus(object sender, RoutedEventArgs e)
{
TreeViewItem item = sender as TreeViewItem;
if(((MyViewModel)item.Content).SomeColor == "Blue")
{
Grid g = VisualTreeHelper.GetChild(item, 0) as Grid;
g.Background = Colors.Blue;
}
}
GotFocus is a RoutedEvent which will bubble up the visual tree if fired. So catch the event somewhere and inspect which was the original source object that fired the event. Or inspect what was the ViewModel propery of the object which fired the event.

How to access an inner ItemsControl in a ListBox in Windows Phone Silverlight

I am building a small Windows Phone application which has a databound ListBox as a main control. DataTemplate of that ListBox is a databound ItemsControl element, which shows when a person taps on a ListBox element.
Currently, I am accessing it by traversing the visual tree of the application and referencing it in a list, and than getting the selected item through SelectedIndex property.
Is there a better or more effective way?
This one works currently, but I am afraid if it would stay effective in case of larger lists.
Thanks
Have you tried wiring the SelectionChanged event of the ListBox?
<ListBox ItemsSource="{Binding}" SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- ... -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
With this in the code behind:
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox listBox = sender as ListBox;
// nothing selected? ignore
if (listBox.SelectedIndex != -1)
{
// something is selected
}
// unselect the item so if they press it again, it takes the selection
listBox.SelectedIndex = -1;
}
ListBoxItem item = this.lstItems.ItemContainerGenerator.ContainerFromIndex(yourIndex) as ListBoxItem;
Then you can use the VisualTreeHelper class to get the sub items
var containerBorder = VisualTreeHelper.GetChild(item, 0) as Border;
var contentControl = VisualTreeHelper.GetChild(containerBorder, 0);
var contentPresenter = VisualTreeHelper.GetChild(contentControl, 0);
var stackPanel = VisualTreeHelper.GetChild(contentPresenter, 0) as StackPanel; // Here the UIElement root type of your item template, say a stack panel for example.
var lblLineOne = stackPanel.Children[0] as TextBlock; // Child of stack panel
lblLineOne.Text = "Some Text"; // Updating the text.
Another option is to use services of the GestureServices class available in the WP7 Toolkit.
You'll need to add a GestureListner to the Root Element of your DataTemplate like so:
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Controls:GestureService.GestureListener>
<Controls:GestureListener Tap="GestureListener_Tap" />
</Controls:GestureService.GestureListener>
<TextBlock x:Name="lblLineOne" Text="{Binding LineOne}" />
<TextBlock Text="{Binding LineTwo}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
And in the GestureListener_Tap event handler, you use this snippet.
private void GestureListener_Tap(object sender, GestureEventArgs e)
{
var itemTemplateRoot = sender as StackPanel;
var lbl1 = itemTemplateRoot.Children[0] as TextBlock;
MessageBox.Show(lbl1.Text);
}
I'm not sure how the GestureListner recognize internally the item being tapped but I guess that it uses the VisualTreeHelper, at least this method is more concise.

C#/WPF: Why is tab not focusing properly

I have a tab control
<TabControl Height="Auto" Grid.Row="1" ItemsSource="{Binding Tabs}" IsSynchronizedWithCurrentItem="True">
That is bound to Tabs in the ViewModel. I also used CollectionViewSource to focus tabs
protected ObservableCollection<TabViewModel> _tabs;
protected ICollectionView _tabsViewSource;
public ObservableCollection<TabViewModel> Tabs
{
get { return _tabs; }
}
public void OnTabsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count > 0)
foreach (TabViewModel tab in e.NewItems)
{
tab.CloseRequested += OnCloseRequested;
_tabsViewSource.MoveCurrentTo(tab); // focus newly created tab
}
if (e.OldItems != null && e.OldItems.Count > 0)
foreach (TabViewModel tab in e.OldItems)
tab.CloseRequested -= OnCloseRequested;
}
When I have more that 1 tab, when I create new Tabs, tabs are focused properly
when there are no tabs, new tabs don't seem to be focused properly. notice the tab header
how might I fix this? or what is causing this behavior? the text box (tab content) is shown but the header don't render like its selected
UPDATE
It works with a fresh file/project ... hmm ... must be some related code ... I might redo that part ...
IsSynchronizedWithCurrentItem="True" has no meaning unless you bind your TabControl.ItemsSource to an ICollectionView.
I can't tell if changing your binding from ObservableCollection to ICollectionView will solve your problem, but that is how I have setup my databound tabcontrol.
An alternative could be to expose a new property
public TabViewModel CurrentTabViewModel
{
get
{
return _tabs.CurrentItem as TabViewModel:
}
set
{
_tabs.MoveCurrentTo(value);
}
}
and bind TabControl's SelectedItem to CurrentTabViewModel
<TabControl SelectedItem="{Binding Path=CurrentTabViewModel}" ... />
Without the code, that initializes the single-tab-collection, it's just guessing.
A Workaround for you would be setting SelectedIndex of the tabView = 0 -> first tab is selected initially.
<TabControl Height="Auto"
Grid.Row="1"
ItemsSource="{Binding Tabs}"
IsSynchronizedWithCurrentItem="True"
SelectedIndex="0">

Select item programmatically in WPF ListView

I'm unable to figure out how to select an item programmatically in a ListView.
I'm attempting to use the listview's ItemContainerGenerator, but it just doesn't seem to work. For example, obj is null after the following operation:
//VariableList is derived from BindingList
m_VariableList = getVariableList();
lstVariable_Selected.ItemsSource = m_VariableList;
var obj =
lstVariable_Selected.ItemContainerGenerator.ContainerFromItem(m_VariableList[0]);
I've tried (based on suggestions seen here and other places) to use the ItemContainerGenerator's StatusChanged event, but to no avail. The event never fires. For example:
m_VariableList = getVariableList();
lstVariable_Selected.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);
lstVariable_Selected.ItemsSource = m_VariableList;
...
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
//This code never gets called
var obj = lstVariable_Selected.ItemContainerGenerator.ContainerFromItem(m_VariableList[0]);
}
The crux of this whole thing is that I simply want to pre-select a few of the items in my ListView.
In the interest of not leaving anything out, the ListView uses some templating and Drag/Drop functionality, so I'm including the XAML here. Essentially, this template makes each item a textbox with some text - and when any item is selected, the checkbox is checked. And each item also gets a little glyph underneath it to insert new items (and this all works fine):
<DataTemplate x:Key="ItemDataTemplate_Variable">
<StackPanel>
<CheckBox x:Name="checkbox"
Content="{Binding Path=ListBoxDisplayName}"
IsChecked="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListViewItem}}, Path=IsSelected}" />
<Image ToolTip="Insert Custom Variable" Source="..\..\Resources\Arrow_Right.gif"
HorizontalAlignment="Left"
MouseLeftButtonDown="OnInsertCustomVariable"
Cursor="Hand" Margin="1, 0, 0, 2" Uid="{Binding Path=CmiOrder}" />
</StackPanel>
</DataTemplate>
...
<ListView Name="lstVariable_All" MinWidth="300" Margin="5"
SelectionMode="Multiple"
ItemTemplate="{StaticResource ItemDataTemplate_Variable}"
SelectionChanged="lstVariable_All_SelectionChanged"
wpfui:DragDropHelper.IsDropTarget="True"
wpfui:DragDropHelper.IsDragSource="True"
wpfui:DragDropHelper.DragDropTemplate="{StaticResource ItemDataTemplate_Variable}"
wpfui:DragDropHelper.ItemDropped="OnItemDropped"/>
So what am I missing? How do I programmatically select one or more of the items in the ListView?
Bind the IsSelected property of the ListViewItem to a property on your model. Then, you need only work with your model rather than worrying about the intricacies of the UI, which includes potential hazards around container virtualization.
For example:
<ListView>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsGroovy}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
Now, just work with your model's IsGroovy property to select/deselect items in the ListView.
Where 'this' is the ListView instance. This will not only change the selection, but also set the focus on the newly selected item.
private void MoveSelection(int level)
{
var newIndex = this.SelectedIndex + level;
if (newIndex >= 0 && newIndex < this.Items.Count)
{
this.SelectedItem = this.Items[newIndex];
this.UpdateLayout();
((ListViewItem)this.ItemContainerGenerator.ContainerFromIndex(newIndex)).Focus();
}
}
Here would be my best guess, which would be a much simpler method for selection. Since I'm not sure what you're selecting on, here's a generic example:
var indices = new List<int>();
for(int i = 0; i < lstVariable_All.Items.Count; i++)
{
// If this item meets our selection criteria
if( lstVariable_All.Items[i].Text.Contains("foo") )
indices.Add(i);
}
// Reset the selection and add the new items.
lstVariable_All.SelectedIndices.Clear();
foreach(int index in indices)
{
lstVariable_All.SelectedIndices.Add(index);
}
What I'm used to seeing is a settable SelectedItem, but I see you can't set or add to this, but hopefully this method works as a replacement.
In case you are not working with Bindings, this could also be a solution, just find the items in the source and add them to the SelectedItems property of your listview:
lstRoomLights.ItemsSource = RoomLights;
var selectedItems = RoomLights.Where(rl => rl.Name.Contains("foo")).ToList();
selectedItems.ForEach(i => lstRoomLights.SelectedItems.Add(i));

Categories

Resources