WPF, set focus on TabControl's content - wpf

I'd like to set focus to TabControl's current selected TabItem's content.
I'm binding items via ItemsSource, so I don't have simple access to TabItems themselves;
I also cannot call Focus() on TabControl, because it focuses the TabControl itself instead of its content (for this one I did a crude, but effective check - made a DispatcherTimer, which emitted current focused item once a second).
Effectively I want to achieve the same effect as clicking on currently selected tab's header. How can I do that (not doing that quick&dirty by simulating the click, of course)?

You could extend your TabControl and override the OnSelectionChanged method.
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
//TODO: Ignore this event on load.
if (e.AddedItems.Count > 0)
((e.AddedItems[0] as TabItem)?.Content as UIElement)?.Focus();
base.OnSelectionChanged(e);
}

Related

Master-detail: How to fetch a control from a template inside the "detail" ContentControl?

I have a ListView (on the 'master' side) whose selection drives a ContentControl's Content property (on the 'detail' side). The ContentControl's visual tree comes from either of two DataTemplate resources that use DataType to choose which detail view to render based on what is selected in the ListView.
That part works fine.
The part I'm struggling with is that there is a particular control inside (one of) the templates that I need to obtain a reference to whenever it changes (e.g. the template selected changes or the ListView selection changes such that the instance of the control is recreated.)
In my ListView.SelectionChanged event handler, I find the ContentControl has not yet been updated with its new visual tree, so initially it's empty on the first selection, and for subsequent selections its visual tree matches the old selection instead of the new one.
I've tried delaying my code by scheduling on the Dispatcher with a priority as low as DispatcherPriority.Loaded, which works for the first selection but on subsequent selections my code still runs before the visual tree is updated.
Is there a better event I should be hooking to run whenever the ContentControl's visual tree is changed to reflect a changed data-bound value to its Content property?
Extra info: the reason I need to reach into the expanded DataTemplate is that I need to effectively set my view model's IList SelectedItems property to a DataGrid control's SelectedItems property. Since DataGrid.SelectedItems is not a dependency property, I have to do this manually in code.
The fix required a combination of techniques. For the first selection that populates the visual tree, I needed to handle ContentControl.OnApplyTemplate() which is only a virtual method rather than an event. I derived from it and exposed it as an event:
public class ContentControlWithEvents : ContentControl
{
public event EventHandler? TemplateApplied;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.TemplateApplied?.Invoke(this, EventArgs.Empty);
}
}
In the XAML I used the above class rather than ContentControl:
<local:ContentControlWithEvents
Content="{Binding SelectedAccount}"
x:Name="BankingSelectedAccountPresenter"
TemplateApplied="BankingSelectedAccountPresenter_TemplateApplied" />
Then I handle the event like this:
void BankingSelectedAccountPresenter_TemplateApplied(object sender, EventArgs e) => this.UpdateSelectedTransactions();
private void UpdateSelectedTransactions()
{
if (this.MyListView.SelectedItem?.GetType() is Type type)
{
DataTemplateKey key = new(type);
var accountTemplate = (DataTemplate?)this.FindResource(key);
Assumes.NotNull(accountTemplate);
if (VisualTreeHelper.GetChildrenCount(this.BankingSelectedAccountPresenter) > 0)
{
ContentPresenter? presenter = VisualTreeHelper.GetChild(this.BankingSelectedAccountPresenter, 0) as ContentPresenter;
Assumes.NotNull(presenter);
presenter.ApplyTemplate();
var transactionDataGrid = (DataGrid?)accountTemplate.FindName("TransactionDataGrid", presenter);
this.ViewModel.Document.SelectedTransactions = transactionDataGrid?.SelectedItems;
}
}
}
Note the GetChildrenCount check that avoids an exception thrown from GetChild later if there are no children yet. We'll need that for later.
The TemplateApplied event is raised only once -- when the ContentControl is first given its ContentPresenter child. We still the UpdateSelectedTransactions method to run when the ListView in the 'master' part of the view changes selection:
void BankingPanelAccountList_SelectionChanged(object sender, SelectionChangedEventArgs e) => this.UpdateSelectedTransactions();
On initial startup, SelectionChanged is raised first, and we skip this one with the GetChildrenCount check. Then TemplateApplied is raised and we use the current selection to find the right template and search for the control we need. Later when the selection changes, the first event is raised again and re-triggers our logic.
The last trick is we must call ContentPresenter.ApplyTemplate() to force the template selection to be updated before we search for the child control. Without that, this code may still run before the template is updated based on the type of item selected in the ListView.

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();
}
}

How do I hook an event to a control that comes in dynamically as part of a DataTemplate?

I have a ListBox that uses DataTemplateSelector to dynamically decide what
template to use based on the type of the item in the list. I now want to hook
the events that could be fired by controls within the DataTemplate. For example,
if one of the templates has a checkbox in it, I want the application using the
control to be notified when the checkbox is checked. If a different template has
a button within it, I want to be notified when the button is clicked.
Also, since its a ListBox, many of the items could have the same template. So
I will need some kind of RoutedEventArgs so I can walk up from OriginalSource to get
some context information to handle the event.
My solution was to use MouseLeftButtonUp. This works fine for TextBlocks, but it looks like CheckBox and Button controls set handled to true, so the event doesnt bubble up. How can I address these
events so I can assign handlers to them in my calling application?
(Also, Silverlight doesn't actually support DataTemplateSelector, so i followed this example to implement it)
If you are defining the templates in the Xaml for the user control where your event handlers are placed then you should simply be able to assign the event handlers in the Xaml.
However in the specific scenario you outline you can also listen for the MouseLeftButtonUp event via the AddHandler method:-
myListBox.AddHandler(UIElement.MouseLeftButtonUpEvent, myListBox_MouseLeftButtonUp, true);
...
private void myListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
//e.OriginalSource available for your inspection
}
Note by using AddHandler and passing true in the third parameter you will get the event regardless of whether it has been handled.

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);
}
}

Resources