2

I have a VSTO (Excel or Word) addin with a WPF dialog window, shown modally. The dialog has a TextBox that I need to be initially focused. I can get it to focus using various methods like the FocusManager or Keyboard classes, or by requesting a Traversal. Or I can simulate a TAB key press via keybd_event() (user32.dll).

The problem is with any of these methods, the field doesn't seem to be "fully" focused. The cursor shows in the TextBox, but it isn't blinking and typing won't work! To solve the problem, I can either:

  1. press TAB once (not programatically) and the cursor will start blinking and I can type; or

  2. Press Alt-Tab to switch to another application, then back. Again, the cursor will start blinking and I can type.

EDIT: SOLUTION

ErrCode's basic approach works reliably, with some modifications. First, instead of doing his idea just once, I do it on all windows. Second, I open the dummy window after my own window loads, not before. Finally, I introduce some delays, without which the approach doesn't work:

// Window or UserControl, works for both or any FrameworkElement technically.
public static class FocusExtensions
    {
        #region FocusElementOnLoaded Attached Behavior

        public static IInputElement GetFocusElementOnLoaded(FrameworkElement obj)
        {
            return (IInputElement)obj.GetValue(FocusElementOnLoadedProperty);
        }

        public static void SetFocusElementOnLoaded(FrameworkElement obj, IInputElement value)
        {
            obj.SetValue(FocusElementOnLoadedProperty, value);
        }

        public static readonly DependencyProperty FocusElementOnLoadedProperty =
        DependencyProperty.RegisterAttached("FocusElementOnLoaded", typeof(IInputElement), typeof(FocusExtensions), new PropertyMetadata(null, FocusElementOnLoadedChangedCallback));

        private static async void FocusElementOnLoadedChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // This cast always succeeds.
            var c = (FrameworkElement) d;
            var element = (IInputElement) e.NewValue;
            if (c != null && element != null)
            {
                if (c.IsLoaded)
                    await FocusFirst(element);
                else
                    c.Loaded += async (sender, args) =>
                        await FocusFirst(element);
            }
        }

        private static async Task FocusFirst(IInputElement firstInputElement)
        {
            var tmpWindow = new Window
            {
                Width = 0,
                Height = 0,
                Visibility = Visibility.Collapsed
            };

            tmpWindow.Loaded += async (s, e) =>
            {
                await Task.Delay(1);
                Application.Current?.Dispatcher?.Invoke(() =>
                {
                    tmpWindow.Close();
                    firstInputElement.Focus();
                });
            };

            await Task.Delay(1);
            Application.Current?.Dispatcher?.Invoke(() =>
            {
                tmpWindow.ShowDialog(); 
            });
        }

        #endregion FocusElementOnLoaded Attached Behavior
    }

To apply, in your XAML's UserControl or Window, add:

FocusExtensions.FocusElementOnLoaded="{Binding ElementName=MyTextBox}"

The MyTextBox must of course be derived from IInputElement.

Chris Bordeman
  • 255
  • 7
  • 19

5 Answers5

2

Noticed that none of the focus methods you mentioned seem to be the simplest case of Control.Focus(). So for example:

// Constructor of your window
public MyWindow()
{
    // ...
    // set any DataContext before InitializeComponent() if needed
    // ...
    InitializeComponent();
    // ...
    // add anything else you need to do within the constructor of the window
    // ...
    textbox1.Focus();   // Set focus on your text box
}

This was tested in an Excel VSTO add-in project and it immediately allows typing to go to the focused textbox as soon as the dialog is shown. Use with your window Owner hack (had to do similar in my project).

Difference between Control.Focus() and FocusManager.SetFocusedElement()

EDIT

Here's what I found after starting a VSTO add-in project from scratch. It appears to be one of many quirks with using WPF within an Excel VSTO add-in, where you can't reliably get Control.Focus() to work if it is used with the first WPF window ever created.

Workaround: Create a dummy WPF window. Close it. And then create the real one that will use Control.Focus().

Never noticed the problem before since my project always had a short "Loading" window open and close itself before presenting the real window.

bool isFirstTime = true;
private void button1_Click(object sender, RibbonControlEventArgs e)
{
    if (isFirstTime)
    {
        // For some reason the Control.Focus() is unreliable for the first WPF window ever shown within the VSTO addin. :( 
        // So make a dummy one once before we open the real window...
        // NOTE: The reason why I never noticed such a problem before in my own project, is since my project always showed a short loading window that closed itself
        // before the main window is shown. So I could use Control.Focus() in the main window without issues
        var pretendLoadWindow = new Window();
        pretendLoadWindow.SizeToContent = SizeToContent.WidthAndHeight;
        pretendLoadWindow.Visibility = Visibility.Collapsed;
        pretendLoadWindow.Loaded += (_sender, _e) => pretendLoadWindow.Close();
        pretendLoadWindow.ShowDialog();
        isFirstTime = false;
    }
    var window = new Window();
    var excelHwnd = m_ExcelApplication != null ? new IntPtr(m_ExcelApplication.Hwnd) : Process.GetCurrentProcess().MainWindowHandle;
    WindowInteropHelper interopHelper = new WindowInteropHelper(window)
    {
        Owner = excelHwnd
    };
    window.Content = new UserControl1();
    window.SizeToContent = SizeToContent.WidthAndHeight;
    // FYI: just in case you have any breakpoints that switch the focus away from the Excel (to your VS IDE), then when this window is shown it won't typically get focus. Below should fix this...
    window.Loaded += (_sender, _e) => window.Focus();       
    window.ShowDialog();
}

Full test code accessible from here

ErrCode
  • 186
  • 2
  • 11
  • No luck, I've tried every way to focus stuff known to man. I can get the textbox to appear focused, with the keyboard cursor INSIDE the box, just not blinking. If you type the text does NOT go into Excel, it just doesn't go into the textbox either until you press TAB, then it starts blinking. – Chris Bordeman Jul 10 '19 at 12:48
  • @ChrisBordeman Had you not asked this, I would have never investigated and become aware of this quirk. I just assumed everything was working as expected since I always had a splash load window show. Anyway check my edit! Hope it helps. – ErrCode Jul 11 '19 at 11:28
  • That's a great idea, but sadly, it doesn't work in my case. I actually open a loading window, too, before this one. Still, I tried opening a hidden window like your code, with no effect. Then I tried opening the fake window on the Loaded of my own window (after), still no luck. VSTO + WPF really is quirky! – Chris Bordeman Jul 12 '19 at 13:00
  • That's unfortunate. I would not be surprised if this is something to do with any of these factors: .Net framework version (I have 4.6.2 installed) / VSTO version / Office version. Might be worth making sure you have latest patches for your Office at least. – ErrCode Jul 12 '19 at 13:47
  • It works! I open the dummy window after my own window (every time not just once), and I have to introduce async 1ms delays, but I got it working. Thanks ErrCode. – Chris Bordeman Jul 12 '19 at 15:40
  • That's great! Since you seem to use a slightly different order to fix the problem and you also had to introduce a 1ms delay - I'm somewhat concerned about the robustness of our solutions. Definitely seems like there's a race condition somewhere when using VSTO + WPF and setting the focus. Will try your idea out when I have some time. I do wonder whether perhaps there's a universal way (e.g. no difference in order) that works for both our machines equally well. – ErrCode Jul 16 '19 at 04:53
  • Yeah, I found no event or way to avoid the delays and async. I refactored into an attached property. Let me know if it works for you as-is. Thanks again for the help. – Chris Bordeman Jul 17 '19 at 11:24
1

The field doesn't seem to be "fully" focused. The cursor shows in the TextBox, but it isn't blinking and typing won't work!

You're seeing the effects of message pumps misbehaving:

Excel CustomTaskPane with WebBrowser control - keyboard/focus issues

BUG: Cant choose dates on a DatePicker that fall outside a floating VSTO Add-In

VSTO WPF ContextMenu.MenuItem Click outside a TaskPane not raised

The bug is to do with controls that respond to input and the void WndProc(ref Message m) messages being incorrectly filtered or redirected in the dispatch loop. To solve it you have to tap into the Message Loop, eg:

protected override void WndProc(ref Message m)
{
  const int WM_PARENTNOTIFY = 528;
  if(m.Msg == WM_PARENTNOTIFY && !this.Focused)
  {
    this.Focus();
  }
  base.WndProc(ref m);
}

This is similar to what @Ian answered in the link you referenced in your other question ElementHost blocks mouse events.


Here is a summary of the behaviour by Hans Passant:

What's never not a problem (ie can often be problematic) is that you rely on the message pump in Excel to dispatch Windows messages, the messages that make these controls respond to input. This goes wrong in WPF as much as Winforms, they have their own dispatch loop that filters messages before they are delivered to the window. Key things that go wrong when their respective dispatcher isn't used are stuff like tabbing and short-cut keystrokes.

And then some, this kind of problem would be induced by Excel doing its own filtering before dispatching messages. I'd guess at an anti-malware feature, Microsoft is forever worried about programs messing with Office apps.

Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
0

It seems you just need to set the owner of your WPF window. To get the job done you need to initialize the WindowInteropHelper with a WPF window object for the dialog box. You can then get the WPF window's handle (HWND) from the Handle property and specify the owner for the WPF window with the Owner property. The following code example shows how to use WindowInteropHelper when hosting a WPF dialog box in a Win32 application.

  WindowInteropHelper wih = new WindowInteropHelper(myDialog);
  wih.Owner = ownerHwnd;
  myDialog.ShowDialog();
Eugene Astafiev
  • 47,483
  • 3
  • 24
  • 45
0

Instead of:

var hwndOwner = (IntPtr)ExcelInterop.App.Hwnd;

try to use:

new WindowInteropHelper(window) { Owner = Process.GetCurrentProcess().MainWindowHandle };
window.ShowDialog();
developer_hatch
  • 15,898
  • 3
  • 42
  • 75
0

Have you tried the following

1. Try setting the focus inside the Loaded Event of the dialogue. Which will make it focus after the dialogue window is fully loaded.

            private void MyWindow_Loaded(object sender, RoutedEventArgs e)
            {
                myTextBox.Focus();
            }

2. Try setting a keyboard focus for your control.

           Keyboard.Focus(myTextBox);
Vishnu Babu
  • 1,183
  • 1
  • 13
  • 37
  • Yeah, I tried that, and using FocusManager. And setting the window to a focus context. None of it gets around this oddity. It's so strange the blinking vertical bar is visible inside the TextBox, just not blinking! – Chris Bordeman Jul 09 '19 at 12:07