I have a ContextMenu with the ItemsSource bound to the selected item of a list view, like this:
<ContextMenu ItemsSource="{Binding Path=PlacementTarget.SelectedItem,
RelativeSource={RelativeSource Self}, Converter={StaticResource possibleConverter}}"/>
The possibleConverter enumerates all possible values for a property of the the selected item, which are shown in the context menu. In the Opened event of the context menu, I select the current value like this:
var cm = e.OriginalSource as ContextMenu;
if (cm != null) {
var lv = cm.PlacementTarget as ListView;
var field = lv.SelectedItem as Field;
var item = cm.ItemContainerGenerator.ContainerFromItem(cm.Items.OfType<object>().Where(o => o.ToString().Equals(field.StringValue)).FirstOrDefault()) as MenuItem;
if (item != null) {
item.IsChecked = true;
}
}
Not particularly elegant, but it works. With the debugger I verified that the ContextMenu.Items.Count property has a non-zero value when expected (i.e. cm.Items.Count is non-zero in the if).
So far, so good. There are, however, items in the listview where the context menu will have no items. In this case, an empty menu is shown. I tried to suppress this in the ContextMenuOpening event in the list view, like this:
var lv = sender as ListView;
if (lv != null) {
var cm = lv.ContextMenu;
if ((cm != null) && (cm.Items.Count > 0)) {
// Here we want to check the current item, which is currently done in the Opened event.
} else {
e.Handled = true;
}
}
Seems like it should work. However, cm.Items.Count is always zero. This is true even if ListView.SelectedItem did not change: For an item with menu entries, the menu is shown correctly after the first click, so the data binding has already happend. It is shown correct the second time as well, but in any case, Items.Count is zero in the ContextMenuOpening event.
What am I missing? How can I suppress empty context menus? Why is the count zero in the ContextMenuOpening handler, which is in Windows Forms (ContextMenuStrip.Opening) the canonical point where to do these things?
EDIT: Upon further investigating, it turns out that in the ContextMenuOpening handler, any binding to the listview fails, which is why ItemsSource is null. I tried to bind via ElementName, via a FindAncestor relationship, all to no avail. The PlacementTarget is null during that event. An ugly hack worked though: In the ContextMenuOpening event, I assign the list view to the ContextMenu.Tag property, while the ItemsSource binding now binds to Tag.SelectedItem. This updates the binding, so Items.Count is what it should be. It's still strange. How can you do meaningful things in ContextMenuOpening other than replacing the menu or something, if the binding fails because somehow the context menu is out of context during the event? Was it only tested with static pre-defined menu items?
Related
I have a user control containing an expander. The content of the explander is a ListBox bound to an object, and a DataTemplate displays it correctly. The problem is this: the user can select a Listbox item, and the SelectionChanged handler changed the DataContext of the ListBox to the selected object.
Like this:
<ListBox
Name="RelativesLB" ItemsSource="{Binding Relatives}",
ItemsTemplate ="{...}",
Selectionchanged="Relatives_OnSelectionChanged" />
And:
Relatives_OnSelectionChanged(object sender, ...EventArgs e)
{
var who = (sender as ListBox).SelectedItem as Person;
if (who == null)
return;
People.DataContext = who;
Here is the problem:
The SelectionChanged event fires.
The DataContext is changed, and the ListBox repopulates.
The SelectionChanged event fires with SelectedItem = null. Here, my code does not change the DataContext; it just returns.
the SelectionChanged event fires again with SelectedItem = <whatever is first>. Here, my code changes the DataContext again to that item I don't want this bit. Actually, I want to stop after 2.
the Datacontext is changed to <whatever is first>
...
and so on, until we get an empty Person.Relatives, then we stop.
What I want is the stop after the first DataContext change. You select a person from the Relatives collection, and get the view for that person.
How can I stop the subsequent SelectionChanged events firing?
I guess, in your on Relatives_OnSelectionChanged you need to set
e.Handled = True;
ListBox fires lostfocus event when its ListBoxItem is selected, how to avoid this ?
I want LostFocus to fire only when something else outside it receives focus.
The thing is, ListBox never gets the focus, only it`s items do, so for this to work we`ll have to use them. Probably there`s some elegant solution for this, but here`s my take.
This I placed in code behind of the Window that contains ListBox:
private void myListBox_LostFocus(object sender, RoutedEventArgs e)
{
var focused = FocusManager.GetFocusedElement(this);
var item = focused as ListBoxItem;
if (item == null || !lbMain.Items.Contains(item.DataContext))
{
//do stuff
}
}
The second check in if statement is in case other ListBoxes are present. If your scenario is more complex you most probably will have to add some tweaks: for example if two ListBoxes have the same ItemsSource.
try to set on ListBoxItem the property Focusable to False. Eventually on elements inside your ListBoxItem DataTemplate.
I have an ObservableCollection of "things" in my view model, and a couple filtered subsets of that list in additonal ObservableCollections. I have two DataGrids on the screen, and I have bound them each to one of the subset ObservableCollections.
Both DataGrids have their SelectedItem property bound to a SelectedThing property in the view model.
When I change SelectedThing either programatically or by selecting a row in one of the two grids, it will change as expected. If the item now pointed to by SelectedThing exists in a grid, the grid will update it's selected item.
So here is my problem... if SelectedThing does not exist in the grid's ItemSource, the selection acts like nothing happened and remains in whatever state it was in before SelectedThing was changed. Ideally I would like the selected to Clear if the underlying view model property no longer is set to something in the grid's ItemsSource... anyone have any suggestions?
Ok. Got it working. In case it helps someone else in the future, here's what made it work...
In your code behind, register an event handler for the view model's PropertyChanged event, and then use that to check each grid to see if it contains the item being selected. If not, then clear the selected in that grid. I also modified my SelectedThing property to ignore incoming NULL values to avoid a deadlock (and in my app it will never be NULL after initialization)
_vm is a Property that returns my view model.
_vm.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_vm_PropertyChanged);
void _vm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedThing")
{
CheckSelection(grid1, _vm.SelectedThing);
CheckSelection(grid2, _vm.SelectedThing);
}
}
void CheckSelection(DataGrid grid, object selectedItem)
{
if (grid.ItemsSource != null)
{
bool itemInGrid = false;
foreach (var item in grid.ItemsSource)
{
if (item == selectedItem)
{
itemInGrid = true;
break;
}
}
if (!itemInGrid) // clear selection
{
grid.SelectedItem = null;
// not sure why, but this causes the highlight to clear. Doesn't work otherwise
grid.IsEnabled = false;
grid.IsEnabled = true;
}
}
}
I'm using ObservableCollection to bind data into list box. Is there a way to make first list item to be selected right after data binding? Is there any event I can use ?
Thank you
Right after (or any point after) setting the datacontext for the listbox (or parent object - probably the page), just set the selected index to the first item in the list.
listbox.SelectedIndex = 0;
If you've got a handler for when the selected index is changed then be sure to ignore when you first set the index.
Create a property named IsSelected in the object contained within the ObservableCollection. Bind this to the ListBoxItem's IsSelected property via a TwoWay binding.
Then, in the page's OnLoaded callback (or wherever you're binding the collection to the ListBox), do something like this
foreach( var obj in myCollection ) {
obj.IsSelected = false;
}
if( myCollection.Count > 0 ) {
myCollection[0].IsSelected = true;
}
// bind the collection to the listbox
why won't you try something like
var listBoxItem = ItemContainerGenerator.ContainerFromItem(myList.First());
listBoxItem.Focus();
or
listBoxItem.IsSelected = true;
How can I get the Container for an object in WPF ItemsControl.
I am writing a multiselect treeview with bindable SelectedItem und SelectedItems Dependency Properties. So long everything works just fine. The only thing is, when I click on an item in the tree with pressed ctrl a second time this item should not be selected but the last previous selected item. The TreeView contains a private Method called ChangeSelection. As far as i understand the first parameter is the Container, the second is the TreeViewItem and the last wherether the item shall be selected or not.
I implement the multiselection with catching the SelectedItemChanged event.
This code works for the new selected item
private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var view = ItemContainerGenerator.ContainerFromItem(e.NewValue) as TreeViewItem;
// ...
}
BUT if i want to get the TreeViewItem from an item saved in an ObservableCollection... it will not work.
EDIT: Ok, as i found out. The code above works only for the first level of items...
EDIT: The solution for this problem isn't trivial. It is possible to find the selected treeview item by using a viewmodel (f.e. an interface which provides basics like: IsSelected, IsExpanded, IsEnabled and Parent). You can search the TreeViewItem like this:
if (treeViewItem.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
{
EventHandler eventHandler = null;
eventHandler = delegate
{
treeViewItem.ItemContainerGenerator.StatusChanged -= eventHandler;
// Call the search function recursive XYZ(tree, treeViewItem.ItemContainerGenerator.ContainerFromItem(nextLevelItem) as TreeViewItem);
};
// wait for the containers to be generated
treeViewItem.ItemContainerGenerator.StatusChanged += eventHandler;
}
else
{
// Call the search function recursive XYZ(tree, treeViewItem.ItemContainerGenerator.ContainerFromItem(nextLevelItem) as TreeViewItem);
}