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));
Related
I'm having problems finding a control (ToggleSwitch) inside my ListView. I have tried several approaches found here on SO or on other places around the web but none seems to be working.
Here is a the listView markup
<ListView Name="LampsListView" ItemsSource="{x:Bind Lamps}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Lamp">
<StackPanel Name="StackPanel">
<TextBlock Margin="10,0" Text="{Binding Name}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<ToggleSwitch Margin="10,0" HorizontalAlignment="Right" Name="LampToggleSwitch" IsOn="{x:Bind State, Converter={ StaticResource IntToIsOn}}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I have tried the ContainerFromItem but x will always be null.
foreach (var item in this.LampsListView.Items)
{
var x = this.LampsListView.ContainerFromItem(item);
}
And also the GetChildren approach but even thought GetChildren returns items it wont give me anything I can work with.
private void FindMyStuff()
{
var ch = this.GetChildren(this.LampsListView);
}
private List<FrameworkElement> GetChildren(DependencyObject parent)
{
List<FrameworkElement> controls = new List<FrameworkElement>();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); ++i)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is FrameworkElement)
{
controls.Add(child as FrameworkElement);
}
controls.AddRange(this.GetChildren(child));
}
return controls;
}
And I've tried booth finding the StackPanel and go straight for the LampToggleSwitch.
The FindMyStuff() is called right after i've updated the ObservableCollection that is bound to the ListView and the update is done from a this.Dispatcher.RunAsync(). I don't know if this has anything to do with it thought.
Could someone please tell me what I'm doing wrong?
Generally traversing visual tree or getting items by names/types is in most cases a wrong way of doing thigs, much better would be to implement apropriate binding.
Nevertheless if you want to do this, you are almost there. As I've tried it should work like this:
var listViewItem = this.mylist.ContainerFromItem(mylist.Items.First()) as ListViewItem;
var itemsStackPanel = listViewItem.ContentTemplateRoot as StackPanel;
var myToggleSwitch = itemsStackPanel.Children.FirstOrDefault(x => x is ToggleSwitch);
// other way with your helper
var childByHelper = GetChildren(listViewItem).FirstOrDefault(x => x is ToggleSwitch);
Just watch out when you run this, if it's done before list is populated, listVieItems will be null.
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;
}
I have a ViewModel with two ICollectionViews which are bound as ItemsSources to two different ListBoxes. Both wrap the same ObservableCollection, but with different filters. Everything works fine initially and both ListBoxes appear properly filled.
However when I change an item in the ObservableCollection and modify a property which is relevant for filtering, the ListBoxes don't get updated. In the debugger I found that SourceCollection for both ICollectionVIews is null although my ObservableCollection is still there.
This is how I modify an item making sure that the ICollectionViews are updated by removing and adding the same item:
private void UnassignTag(TagViewModel tag)
{
TrackChangedTagOnCollectionViews(tag, t => t.IsAssigned = false);
}
private void TrackChangedTagOnCollectionViews(TagViewModel tag, Action<TagViewModel> changeTagAction)
{
_tags.Remove(tag);
changeTagAction.Invoke(tag);
_tags.Add(tag);
}
The mechanism works in another context where I use the same class.
Also I realized that the problem disappears if I register listeners on the ICollectionViews' CollectionChanged events. I made sure that I create and modify them from the GUI thread and suspect that garbage collection is the problem, but currently I'm stuck... Ideas?
Update:
While debugging I realized that the SourceCollections are still there right before I call ShowDialog() on the WinForms Form in which my UserControl is hosted. When the dialog is shown they're gone.
I create the ICollectionViews like this:
AvailableTags = new CollectionViewSource { Source = _tags }.View;
AssignedTags = new CollectionViewSource { Source = _tags }.View;
Here's how I bind one of the two (the other one is pretty similar):
<ListBox Grid.Column="0" ItemsSource="{Binding AvailableTags}" Style="{StaticResource ListBoxStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource ListBoxItemBorderStyle}">
<DockPanel>
<Button DockPanel.Dock="Right" ToolTip="Assign" Style="{StaticResource IconButtonStyle}"
Command="{Binding Path=DataContext.AssignSelectedTagCommand, RelativeSource={RelativeSource AncestorType={x:Type tags:TagsListView}}}"
CommandParameter="{Binding}">
<Image Source="..."/>
</Button>
<TextBlock Text="{Binding Name}" Style="{StaticResource TagNameTextBlockStyle}"/>
</DockPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I use MvvmLight's RelayCommand<T> as ICommand implementation in my ViewModel:
AssignSelectedTagCommand = new RelayCommand<TagViewModel>(AssignTag);
I had this issue too, with a similar use-case. When I updated the underlying collection, I would call Refresh() on all the filtered views. Sometimes, this would result in a NullReferenceException thrown from within ListCollectionView.PrepareLocalArray() because SourceCollection is null.
The problem is that you shouldn't be binding to the CollectionView, but to the CollectionViewSource.View property.
Here's how I do it:
public class ViewModel {
// ...
public ViewModel(ObservableCollection<ItemViewModel> items)
{
_source = new CollectionViewSource()
{
Source = items,
IsLiveFilteringRequested = true,
LiveFilteringProperties = { "FilterProperty" }
};
_source.Filter += (src, args) =>
{
args.Accepted = ((ItemViewModel) args.Item).FilterProperty == FilterField;
};
}
// ...
public ICollectionView View
{
get { return _source.View; }
}
// ...
}
The reason for your issue is that the CollectionViewSource is getting garbage collected.
Ok, this is a strange one (or I'm doing something stupidly).
I have a WPF combobox which is populated with a string array on form load (app start).
This part works fine. The sticky bit is when I try to alter any of the information within said combobox. The debug says that it is changing but nothing is being shown visually.
// Populate the combobox:
private void ComboBlocks()
{
comboBox1.Items.Clear();
string[,] _tmp = _kits.BlockIDNames;
string[] _tmp1 = new string[_tmp.GetLength(0)];
for (int i = 0; i < _tmp.GetLength(0); i++)
{
_tmp1[i] = _tmp[i, 0] + " - " + _tmp[i, 1];
}
foreach (string s in _tmp1)
{
string[] _tmpS1 = s.Split(new char[] { '-' });
int _tmpS2 = Convert.ToInt32(_tmpS1[0].Trim());
bool _banneditem = _cbi.BannedItemExists(_tmpS2);
if (_banneditem == true)
AddComboItem(s, true);
else
AddComboItem(s);
}
if (comboBox1.Items.Count > 0)
comboBox1.SelectedIndex = 0;
}
// Add item to combobox:
private void AddComboItem(string _text,bool _redtext = false)
{
Grid grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
TextBlock text = new TextBlock();
text.Text = _text;
if (_redtext == true)
text.Foreground = Brushes.Red;
else
text.Foreground = Brushes.Black;
grid.Children.Add(text);
Grid.SetColumn(text, 0);
ComboBoxItem comboBoxItem = new ComboBoxItem();
comboBoxItem.Content = grid;
comboBoxItem.Tag = _text;
comboBox1.Items.Add(comboBoxItem);
}
Also, I am fairly new to C# so if there is anything that I'm doing wrong/inefficiently, please point it out.
Many thanks.
EDIT: Iterating through each one and changing the text value would probably be just as much work as it gets its information from another array. It would have to check for each item in the array and if it exists, colour it red, if it doesn't, colour it black.
I can't see what, specifically, you're doing wrong here. You can probably get that to work if you hack around on it enough.
But it's not the right approach - at least, not as far as writing maintainable, reliable WPF code is concerned. You should be using data binding to populate this control. You will find, after very little experience with doing this, that it makes for much faster, easier development than the code-WPF-as-if-it-were-WinForms approach you're taking.
Here's how:
Create a class for holding your data items, called, e.g. Block. Have it expose string Text and bool IsBanned properties.
Create an ObservableCollection<Block> and populate it with new Block objects created from your data source.
Expose the collection to binding. There are a lot of ways to do this; the example below assumes you've added it to the window's resource dictionary with a key of Blocks. You could also implement a Blocks property in your window, or (what I do pretty much any time I create a window or user control in WPF) create a class that exposes a Blocks property and set the window's DataContext to an instance of that class.
Now put this in your window's XAML:
<ComboBox ItemsSource="{Binding {DynamicResource Blocks}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Text}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsBanned}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
You don't actually need that Grid in there (nor do you need to set Grid.Column, since it defaults to 0); I just put that in there so that the example would more accurately replicate what's in your code. Also, I'm not setting the Tag property on the ComboBoxItem, but that's because the ComboBox's SelectedItem contains the actual Block instance for that item, which obviates the need for the Tag property.
Since you're using an ObservableCollection, any changes that you make to the collection (i.e. adding/removing/reordering its items) will automatically be reflected in what's on the screen; binding will take care of that for you.
If the items' Text or IsBanned properties will be changing after you populate the collection, and you need the ComboBox to reflect these changes, you'll need to implement INotifyPropertyChanged in the Block class and have it raise PropertyChanged in the setters of those properties.
I have a ListView that is bound to an ObservableCollection which is itself derived from an IQueryable. When I refresh the collection, after items are added, edited or a button is clicked (to see if the database has new items from other users). The listbox refreshes and selects the first item in the collection.
I want to keep the same selected item (If it still exists) after a refresh but because the Collection has been replaced I cannot seem to compare equality between an item in the old collection and the new one. All attemtps never have a match.
How should this be done?
Below are some relevant code snipets if anyone wants to see them:
The ListView, MouseDoubleClick Event opens an edit window
<ListView x:Name="IssueListView"
ItemsSource="{Binding Issues}"
ItemTemplate="{StaticResource ShowIssueDetail}"
IsSynchronizedWithCurrentItem="True"
MouseDoubleClick="IssueListView_MouseDoubleClick" />
It's DataTemplate
<DataTemplate x:Key="ShowIssueDetail">
<Border CornerRadius="3" Margin="2" MinWidth="400" BorderThickness="2"
BorderBrush="{Binding Path=IssUrgency,
Converter={StaticResource IntToRYGBBorderBrushConverter}}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=IssSubject}"
Margin="3" FontWeight="Bold" FontSize="14"/>
<!--DataTrigger will collapse following panel for simple view-->
<StackPanel Name="IssueDetailPanel" Visibility="Visible" Margin="3">
<StackPanel Width="Auto" Orientation="Horizontal">
<TextBlock Text="Due: " FontWeight="Bold"/>
<TextBlock Text="{Binding Path=IssDueDate, StringFormat='d'}"
FontStyle="Italic" HorizontalAlignment="Left"/>
</StackPanel>
<StackPanel Width="Auto" Orientation="Horizontal">
<TextBlock Text="Category: " FontWeight="Bold"/>
<TextBlock Text="{Binding Path=IssCategory}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=StatusBoardViewModel.ShowDetailListItems,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}}" Value="False">
<Setter TargetName="IssueDetailPanel"
Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
The Issues collection is refreshed by executing a QueryIssues Method that builds a Linq To SQL query programatically based on bound controls. IssuesQuery is an IQueryable property
public void QueryIssues()
{
// Will show all items
IssuesQuery = from i in db.Issues
orderby i.IssDueDate, i.IssUrgency
select i;
// Filters out closed issues if they are not to be shown
if (includeClosedIssues == false)
{
IssuesQuery = from i in IssuesQuery
where i.IssIsClosed == false
select i;
}
// Filters out Regular Tasks if they are not to be shown
if (showTasks == false)
{
IssuesQuery = from i in IssuesQuery
where i.IssIsOnStatusBoard == true
select i;
}
// Filters out private items if they are not to be shown
if (showPrivateIssues == false)
{
IssuesQuery = from i in IssuesQuery
where i.IssIsPrivate == false
select i;
}
// Filters out Deaprtments if one is selected
if (departmentToShow != "All")
{
IssuesQuery = from i in IssuesQuery
where i.IssDepartment == departmentToShow
select i;
}
Issues = new ObservableCollection<Issue>(IssuesQuery);
}
Because you are getting completely new objects there is no other way to match equality than to find the item that matches the old (if it exists) and select it. You are replacing your collection which means that you have to track the selected item yourself.
Other options:
You could keep a collection and manually add/remove items based on the query results (ie don't destroy the current item).
One way to do this in your view model:
// The ListView's SelectedItem is bound to CurrentlySelectedItem
var selectedItem = this.CurrentlySelectedItem;
// ListView is bound to the Collection property
// setting Collection automatically raises an INotifyPropertyChanged notification
this.Collection = GetIssues(); // load collection with new data
this.CurrentlySelectedItem = this.Collection.SingleOrDefault<Issue>(x => x.Id == selectedItem.Id);
When faced with this issue in the past i have wrapped the items i want to display in a small view model class, then given each an extra property IsSelected and then bound this property to the ListBoxItem using an ItemContainerStyle. That way i keep track of what is selected.