TabControl losing selected tab on Window.Show - wpf

We have an MVVM (Cinch) solution that has a Window with a TabControl in it. The ItemsSource is bound to a CollectionView (DefaultView generated from an ObservableCollection), with IsSynchronizedWithCurrentItem=true. Everything works great the first time the Window loads... tabs are displaying correctly and the user can switch between them.
The problem occurs when the Window is hidden and shown again. The first tab is always selected again, regardless of what the CurrentItem in the CollectionView is. The line before the .Show has the CurrentItem as the tab we want, but the tab switches during the .Show operation.
Has anyone else run into this issue?

This is definitely a hack. But you could override the Activated event and store the tab index before it gets reset and set it again when it reactivates.
protected override void OnActivated(EventArgs e)
{
int tabControlIndex = myTabController.SelectedIndex;
base.OnActivated(e);
myTabController.SelectedIndex = tabControlIndex ;
}

Related

WPF SelectedItem binding to source doesn't affect keyboard navigation

OK, so I'm using a typical Binding to my ViewModel. It works beautifully, to source or to target, or so it seems. The vm collection is an ObservableCollection which is initialized and never modified (no setter).
public ObservableCollection<Statement> StatementsList { get; } = new();
#region SelectedStatement
private Statement _selectedStatement;
public Statement SelectedStatement
{
get => _selectedStatement;
set => Set(ref _selectedStatement, value, nameof(SelectedStatement));
}
#endregion SelectedStatement
I can set SelectedStatement from the ViewModel, and the UI updates fine. I can watch the SelectionChanged event of the DataGrid and confirm the added items and removed items are exactly as expected.
Then, I select a different row USING THE MOUSE, and use my search function to select another row using SelectedItem = some statement, which visually selects the row perfectly (again), confirmed by the SelectionChanged event again. SelectedStatement in my view model has the correct value!
Then, the weirdness starts. I press the down arrow the keyboard.
You'd expect the next line after the selected statement to be selected, but instead the next line after the previously selected item (using the mouse) is selected. It's like the keyboard responding code in the DataGrid is not recognizing the prior new row selection via the VM.
Has anyone seen this behavior? I've done WPF development for many years, and I've seen many weird WPF bugs, but this one I've never noticed!
Note that IsSynchronizedWithCurrentItem="True" on the DataGrid. I tried setting it to false just as a stab in the dark, but no change in behavior. I also tried changing my SelectedItem property to wrap a call to GetDefaultCollectionView() and getting/changing the selected item via the collection view instead of using a binding to SelectedItem. The behavior is identical.
Selecting an item is essentially setting IsSelected = true.
And setting this property does not affect the Focus transition to the selected element in any way.
And when controlling from the keyboard, the transition occurs from the element with Focus.
You can add the SelectionChanged processing to the Selector (ListBox, DataGrid,...) and in it perform the Focus transition to the selected item (by the index in the SelectedIndex).
An example of such a handler:
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is Selector selector)
{
int index = selector.SelectedIndex;
if (index >=0)
{
var element = selector.ItemContainerGenerator.ContainerFromIndex(index);
if (element is UIElement uiElement)
uiElement.Focus();
}
}
}

Setting Default Keyboard Focus On Loading A UserControl

I have an MVVM setup with a mainwindow that contains a ContentControl.
I set this to a particular viewmodel which then maps to a view.
A view is a usercontrol.
I want to be able to set the default keyboard focus to a default element in the usercontrol(View) when it loads so the application can eventually be driven just by using up, down, left, right and enter.
Some of my failed attempts are setting
FocusManager.FocusedElement="{Binding ElementName=DefaultElement}"
in my content control tag. This sets the logical focus but not the keyboard focus
I'd rather keep the solution in xaml if possable but have tried placing the following in code behind.
Keyboard.Focus(DefaultElement);
This does not work but if I popup a message box first it does. I'm a little confused as to why.
MessageBox.Show(Keyboard.FocusedElement.ToString());
Keyboard.Focus(DefaultElement);
EDIT::::
I just placed this in my onloaded event of my user control. It seems to work but can anyone see any issues that might arrise at this priority level. I.E a circumstance when the action will never run?
Dispatcher.BeginInvoke(
DispatcherPriority.ContextIdle,
new Action(delegate()
{
Keyboard.Focus(DefaultElement);
}));
It seems that this wpf the you have to implement a workaround on a case by case basis. The solution that seemed to work best, most of the time for me was to insert the focus code inside the dispatcher when OnVisible was changed. This sets the focus not only when the View/Usercontrol loads but also if you a changing Views by way of Visibility. If you Hide and then Show a ContentControl that is mapped to your ViewModels then the Loaded event won't fire and you'll be forced to Mouse input, or tabbing (Not so good if you want to navigate your app with a remote control).
VisibilityChanged will always fire however. This is what I ended up with for my listbox.
private void ItemsFlowListBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
Dispatcher.BeginInvoke(
DispatcherPriority.ContextIdle,
new Action(delegate()
{
ItemsFlowListBox.Focus();
ItemsFlowListBox.ScrollIntoView(ItemsFlowListBox.SelectedItem);
}));
}
}
I had the same symptom for a WPF UserControl hosted in a Winforms application. Just wanted to note I was about to try this solution when I found a normal TabIndex in the Winforms app fixed it
Per How to set which control gets the focus on application start
"The one with the minimum tab index automatically gets the focus
(assuming the TabStop property is set to true). Just set the tab
indices appropriately."
It's a tricky one with no easy answer. I'm currently doing this, although I'm not sure I like it:
public MyView()
{
InitializeComponent();
// When DataContext changes hook the txtName.TextChanged event so we can give it initial focus
DataContextChanged +=
(sender, args) =>
{
txtName.TextChanged += OnTxtNameOnTextChanged;
};
}
private void OnTxtNameOnTextChanged(object o, TextChangedEventArgs eventArgs)
{
// Setting focus will select all text in the TextBox due to the global class handler on TextBox
txtName.Focus();
// Now unhook the event handler, since it's no longer required
txtName.TextChanged -= OnTxtNameOnTextChanged;
}
And in case you're wondering what the global class handler does, it's this:
protected override void OnStartup(StartupEventArgs e)
{
...
// Register a global handler for this app-domain to select all text in a textBox when
// the textBox receives keyboard focus.
EventManager.RegisterClassHandler(
typeof (TextBox), UIElement.GotKeyboardFocusEvent,
new RoutedEventHandler((sender, args) => ((TextBox) sender).SelectAll()));
which auto selects TextBox text when receiving keyboard focus.

Prevent WPF ListBox from selecting item under mouse when layout changes

If a WPF ListBox gets a MouseMove event while the mouse button is held down, it will change the listbox's selection. That is, if you click the mouse on item #1, and then drag over item #2, it will deselect item #1 and select item #2 instead. How can I prevent this?
That's the short version. The slightly longer version is this: When the user double-clicks an item in my ListBox, I make other changes to my layout, which includes showing other controls above the ListBox. This moves the ListBox downwards, which means the mouse is now positioned over a different ListBoxItem than it was when the user double-clicked.
Since I make these layout changes in response to the DoubleClick event (which is a mouse-down event), it's very likely that the mouse button will still be pressed when this layout change completes, which means WPF will send the ListBox a MouseMove event (since the mouse's position, relative to the ListBox, has changed). ListBox treats this as a drag, and selects the event that's now under the mouse.
I don't want the selection to change between the time I get the double-click event and the time the user releases the mouse (which might be well after the layout changes). I suspect that the simplest way to achieve this would be to disable the "change selection on drag" behavior, but I'm open to other suggestions.
How can I "lock in" the selection on double-click, and prevent it from changing until mouseup?
After some digging around in ILSpy, I found that there's no property that disables the "drag to select" behavior, nor is there an event I can mark as Handled to stop it.
But there is a good inflection point for changing this behavior: ListBoxItem.OnMouseEnter is virtual, and it calls back into the listbox to change the selection. It doesn't seem to do anything else substantive, so all I need to do is override it and do nothing.
EDIT: As it turns out, the above only keeps the selection from changing while you move the mouse around inside the listbox. It doesn't help if you move the mouse above or below the listbox -- then the auto-scroll kicks in and moves the selection. Most of the auto-scroll code is again in non-virtual methods; it looks like the best way to prevent auto-scroll is probably to disable mouse capture. Another override on ListBoxItem can take care of this.
It looks like the best way to use my own ListBoxItem descendant is to descend from ListBox. The final code looks something like this:
public class ListBoxEx : ListBox
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ListBoxExItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ListBoxExItem;
}
}
public class ListBoxExItem : ListBoxItem
{
private Selector ParentSelector
{
get { return ItemsControl.ItemsControlFromItemContainer(this) as Selector; }
}
protected override void OnMouseEnter(MouseEventArgs e)
{
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
ParentSelector?.ReleaseMouseCapture();
}
}

WPF TabControl Switch behaviour

I have a tabcontrol which binds to an observable collection of tabs.
The tabcontrol always has the first tab which hosts a listview bound to another observable collection.
On selecting an item in that list view a new tab is created an focus given to it.
The problem I am having is:
When I switch back to the first tab there is a pause while it redraws / creates the listview items (contains images so slow)
The item selected before moving to the new tab is nolonger selected. Instead the listview is at the top with no item selected.
Can someone please explain to me how the tabcontrol operates is it really distroying the tab item content each time? and how I can instead have a behaviour where the item remains selected when I return to that tab?
Update:
I have confirmed by adding debug print messages to events that no events fire on this switch-back and forth but the first tab is being unloaded - more specifically the usercontrol hosted in that tab is??.
It sounds like the ObservableCollection is the culprit. If you are changing the collection items to control the display, then every time the collection changes won't it redraw the entire tab collection?
Instead, why not maintain the TabItem collection directly? You could then manage the Visibility property of the TabItems to display them or not.
First I needed to ensure my listview bound to my collection correctly i.e. the item stayed selected by adding the property:
IsSynchronizedWithCurrentItem="True"
I then added a loaded event handler to the listview so the item is scrolled into view on switching back:
private void ListView_Loaded(object sender, RoutedEventArgs e)
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(DataContext);
if (collectionView != null)
{
ItemControl.ScrollIntoView(collectionView.CurrentItem);
}
}

Maintain scroll position on updating the ItemSource of a silverlight datagrid

I'm using a DataGrid in my silverlight application to display some data that's refreshed on a timer. My problem is that when this happens the vertical scrollbar in the grid resets to the top, whereas I want it to stay in the same position. Does anyone know how I can make this happen?
I've tried overriding the ItemsSource property on the grid to store the vertical scroll position and then reset it, but this only affects the scrollbar and doesn't force the correct rows to be displayed. Is there a way to force this behaviour?
Here is a similar question about Setting the scroll bar position on a ListBox
After rebinding Silverlight Listbox control how do you get it listbox to scroll to back to the top?
Since the DataGrid also supports a ScrollIntoView method, you should be able to use a similar technique such as
theDataGrid.ItemsSource = data;
theDataGrid.UpdateLayout();
theDataGrid.ScrollIntoView(theDataGrid.SelectedItem, theDataGrid.Columns[0]);
I couldn't find a decent answer last time I looked. I wanted to keep the current element selected in the grid but that wouldn't work on an ICollectionView refresh (I use MVVM and get automatic updates from the server).
ScrollIntoView() was not an option for me because the currently selected item may NOT be in view. Having the CurrentChanged event firing out of control was also quite a bother.
In the end, I used the Infragistics grid and it does just that out of the box. Problem solved for me.
You may have a look at the DevExpress free grid. I think it had the same nice behaviour (I tested it but I can't remember the outcome).
You could try setting the SelectedItem thro the UI thread, so that the UI can refresh itself,
like so
private void Button_Click(object sender, RoutedEventArgs e)
{
Person p = new Person() { Name="sss",Age=11}; //datagird's itemsSource is Collection<person>
people.Add(p);
dg.SelectedItem = p; //dg is my datagrid name
Dispatcher.BeginInvoke(() => { dg.SelectedItem = p; });
}
Im assuming that new rows are loaded thro the ViewModel, so thats why it makes sense to place the BeginInvoke there. Since the ViewModel operations run on a different thread, and just setting the SelectedItem on its own might not work, this has worked for someone else
I've also had issues with this. I solved it by remembering the item I want to scroll to, then re-binding the DataGrid. I handle the LayoutUpdated event in order to implement the desired functionality:
void MyDataGrid_LayoutUpdated(object sender, EventArgs e)
{
// Reference the data item in the list you want to scroll to.
object dataItem = yourDataItem;
// Make sure the item is not null and didn't already scroll to the item.
if (dataItem != null && this.dataItemScrolledTo != dataItem)
{
// Remember the item scrolled to.
this.dataItemScrolledTo = dataItem;
// Scroll datagrid to the desired item.
MyDataGrid.ScrollIntoView(dataItem, MyDataGrid.Columns[0]);
}
}
I've modified CodeMaster's solution so that you don't need a class level variable. Put this code in the method that updates the ItemsSource. It will dynamically create the eventhandler, attach it, then detach it.
EventHandler MyDataGrid_LayoutUpdated = null;
MyDataGrid_LayoutUpdated = (s, e) =>
{
MyDataGrid.ScrollIntoView(dataItem, MyDataGrid.Columns[0]);
MyDataGrid.LayoutUpdated -= MyDataGrid_LayoutUpdated;
};
MyDataGrid.LayoutUpdated += MyDataGrid_LayoutUpdated;

Resources