none
[VS Plugin] Unable to access WPF user control in Option Dialog RRS feed

  • Question

  • Hi guys,

    I am working on a VS extensibility tool that would like to have a page in OptionDialog in VS.
    And I am planning to use WPF for UI. I did a test to get myself familiar with the combination of WPF and OptionPage.
    However, it is not working as I expect.

    My current structure of the design is:
    1. A text box textBox in WPF User Control OptionControl
    2. OptionControl is created as as ElementHost in OptionControlHost which is inherited from DialogPage
    3. Override method Window in OptionControlHost to return the ElementHost object.
    4. OptionControlHost is declared in ProvideOptionPage of a OptionTestPackage which is inherited from Package

    The test has only UI, no data binding being implemented in it.
    However, I am in trouble that, when I try to run this project, the textBox is like it is in ReadOnly mode.
    I can't input any text to the box, but I can do focus and delete.

    Is there something that I missed?
    • Moved by Linda Liu Friday, September 25, 2009 10:24 AM VSX related issue (From:Windows Presentation Foundation (WPF))
    Wednesday, September 23, 2009 7:26 AM

Answers

  • Hi, Kane
    I was able to reproduce the issue you are seeing. And I think this issue is a problem when using WPF Control to integrate with the option page. I have reported a feedback through Visual Studio & .NET Connect site:
    (https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=492562#details).
    This issue will be routed to the product unit who works on this specific feature area for triage and resolution. I’ll paste all responses from the product unit in this thread. And you also can click the link and add the feedback to your watchlist ( you need to log in using your account) so that you can get informed as soon as product units respond.  It may be some time before we get response from product unit. Please be patient.
    Thanks
    Chao
    Monday, September 28, 2009 5:54 AM
    Moderator

All replies

  • Hi, Kane
    I was able to reproduce the issue you are seeing. And I think this issue is a problem when using WPF Control to integrate with the option page. I have reported a feedback through Visual Studio & .NET Connect site:
    (https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=492562#details).
    This issue will be routed to the product unit who works on this specific feature area for triage and resolution. I’ll paste all responses from the product unit in this thread. And you also can click the link and add the feedback to your watchlist ( you need to log in using your account) so that you can get informed as soon as product units respond.  It may be some time before we get response from product unit. Please be patient.
    Thanks
    Chao
    Monday, September 28, 2009 5:54 AM
    Moderator
  • It's now 2013, & there still appears to be no real answer to this issue(even though you've marked your own reply as the answer). I'm having a similar problem, though it's explained in a bit more detail in WPF user control in VS 2010 options page.

    Your Connect link also doesn't work when I click on it, so I can't even see what happened in it, or add it to my watch list.

    "I’ll paste all responses from the product unit in this thread" - there are none pasted here, so does that mean there was no response?

    Could you please unmark this issue as "answered", and/or provide a real answer? I see no point in moderators marking a replay as an answer, just to close a question. It's frustrating to think you've found an answer to a problem you're having, only to find it's a moderator's self-marked reply, which doesn't actually answer the question at all.

    "It may be some time before we get response from product unit. Please be patient"

    I think you'll agree that 3+ years is long enough to wait to be considered "patient".


    Yann - LightSwitch Central - Click here for FREE Themes, Controls, Types and Commands
     
    Please click "Mark as Answer" if a reply answers your question. Please click "Vote as Helpful" , if you find a reply helpful.
     
    By doing this you'll help others to find answers faster.

    Tuesday, January 8, 2013 12:56 PM
  • http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.uielementdialogpage.aspx

    If not in 2012 you would need to recreate that. WPF does not handle WM_GETDLGCODE in its built in WndProc and thus doesn't work well hosted in a traditional Win32 dialog window like the tools->options dialog.

    Ryan

    • Proposed as answer by Yann Duran Wednesday, January 9, 2013 2:48 AM
    Tuesday, January 8, 2013 3:40 PM
    Moderator
  • Thanks for the reply Ryan.

    It would have been nice if this had been mentioned in the documentation somewhere, or even in the samples. I've wasted so many hours trying to get this to work, before basically now finding out that it "can't be done", without significant extra programming. Isn't that what the ElementHost control was supposed to handle?

    It would also have been nice if a 2010 implementation of UIElementDialogPage had been created, not just one for 2012. Is this possible?


    Yann - LightSwitch Central - Click here for FREE Themes, Controls, Types and Commands
     
    Please click "Mark as Answer" if a reply answers your question. Please click "Vote as Helpful" , if you find a reply helpful.
     
    By doing this you'll help others to find answers faster.

    Wednesday, January 9, 2013 2:54 AM
  • I don't know much about ElementHost, it appears to be for inter-oping with WinForms. So if you use that you get Win32 -> WinForms -> WPF (plus the reverse on the way back out). In my experience the more interop layers you have the greater the chance for something to go wrong.

    Looking briefly at ElementHost it doesn't appear to handle any of the dialog specific code that it would need to to be hosted inside a Win32 dialog. Dialogs in Win32 (like the Tools->Option dialog) are not normal windows and have special messages you need to handle, specifically WM_GETDLGCODE.

    >It would also have been nice if a 2010 implementation of UIElementDialogPage had been created, not just one for 2012.

    This was created after 2010 was released, we generally don't update MPF with new functionality once it has shipped or else you would find yourself in a nightmare of running on various VS installs with different dot versions of MPF and you code would work on some and throw TypeLoadExceptions on others. I suspect (and vaguely recall) there was simply no one asking how to host WPF Tools Options pages in 2010, at least not in the time frame where we were actively writing code. We (my team) found ourselves wanting to do this post 2010 for some new pages and had to figure out how to do it, since the obvious approach didn't work. That yielded UIElementDialogPage, which we shipped in MPF in the next release.

    >Is this possible?

    Yes, I will check if I can just post the source for UIElementDialogPage here, I don't own it (Microsoft owns all IP, so I can't just unilaterally post things like this). I can't see there being a problem, there is really no 'secret stuff', just code to deal with WM_GETDLGCODE + an apparent focus issue specific to ElementHost that crops up after getting the WM_GETDLGCODE stuff working.

    Ryan

    • Proposed as answer by Yann Duran Friday, January 11, 2013 3:49 AM
    Wednesday, January 9, 2013 5:14 AM
    Moderator
  • That would be great Ryan. Thank you!

    Yann - LightSwitch Central - Click here for FREE Themes, Controls, Types and Commands
     
    Please click "Mark as Answer" if a reply answers your question. Please click "Vote as Helpful" , if you find a reply helpful.
     
    By doing this you'll help others to find answers faster.

    Wednesday, January 9, 2013 3:04 PM
  • using System;
    using System.ComponentModel;
    using System.Runtime.InteropServices;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Forms.Integration;
    using System.Windows.Input;
    using System.Windows.Interop;
    using System.Windows.Media;
    using Microsoft.VisualStudio.Shell;
    
    using Forms = System.Windows.Forms;
    using IWin32Window = System.Windows.Forms.IWin32Window;
    
    namespace <your namespace here>
    {
        /// <summary>
        /// Event args used by <see cref="UIElementDialogPage.DialogKeyPendingEvent"/>.
        /// </summary>
        public class DialogKeyEventArgs : RoutedEventArgs
        {
            internal DialogKeyEventArgs(RoutedEvent evt, Key key) : base(evt)
            {
                Key = key;
            }
    
            /// <summary>
            /// Gets the key being pressed within the UIElementDialogPage.
            /// </summary>
            public Key Key
            {
                get;
                private set;
            }
        }
    
        /// <summary>
        /// Class which is used to seamlessly host WPF content inside a native dialog
        /// running an IsDialogMessage-style message loop.  UIElementDialogPage enables
        /// tabbing into and out of the WPF child HWND, and enables keyboard navigation
        /// within the WPF child HWND.
        /// </summary>
        [ComVisible(true)]
        public abstract class UIElementDialogPage : DialogPage
        {
            /// <summary>
            /// Routed event used to determine whether or not key input in the dialog should be handled by the dialog or by
            /// the content of this page.  If this event is marked as handled, the keypress should be handled by the content,
            /// and DLGC_WANTALLKEYS will be returned from WM_GETDLGCODE.  If the event is not handled, then only arrow keys,
            /// tabbing, and character input will be handled within this dialog page.
            /// </summary>
            public static readonly RoutedEvent DialogKeyPendingEvent = EventManager.RegisterRoutedEvent("DialogKeyPending", RoutingStrategy.Bubble, typeof(EventHandler<DialogKeyEventArgs>), typeof(UIElementDialogPage));
    
            private ElementHost m_elementHost;
    
            static UIElementDialogPage()
            {
                // Common controls that require centralized handling should have handlers here.
                EventManager.RegisterClassHandler(typeof(ComboBox), DialogKeyPendingEvent, (EventHandler<DialogKeyEventArgs>)HandleComboBoxDialogKey);
                EventManager.RegisterClassHandler(typeof(DatePicker), DialogKeyPendingEvent, (EventHandler<DialogKeyEventArgs>)HandleDatePickerDialogKey);
            }
    
            /// <summary>
            /// Returns the handle to the UI control hosted in the ToolsOption page.
            /// </summary>
            [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
            protected override IWin32Window Window
            {
                get
                {
                    if (m_elementHost == null)
                    {
                        m_elementHost = new DialogPageElementHost();
                        m_elementHost.Dock = Forms.DockStyle.Fill;
    
                        UIElement child = Child;
                        if (child != null)
                        {
                            // The child is the root of a visual tree, so it has no parent from whom to 
                            // inherit its TextFormattingMode; set it appropriately.
                            // NOTE: We're setting this value on an element we didn't create; we should consider
                            // creating a wrapping ContentPresenter to nest the external Visual in.
                            TextOptions.SetTextFormattingMode(child, TextFormattingMode.Display);
    
                            HookChildHwndSource(child);
                            m_elementHost.Child = child;
                        }
                    }
    
                    return m_elementHost;
                }
            }
    
            /// <summary>
            /// Gets the WPF child element to be hosted inside the dialog page.
            /// </summary>
            protected abstract UIElement Child
            {
                get;
            }
    
            /// <summary>
            /// Observes for HwndSource changes on the given UIElement,
            /// and adds and removes an HwndSource hook when the HwndSource
            /// changes.
            /// </summary>
            /// <param name="child">The UIElement to observe.</param>
            void HookChildHwndSource(UIElement child)
            {
                // The delegate reference is stored on the UIElement, and the lifetime
                // of the child is equal to the lifetime of this UIElementDialogPage,
                // so we are not leaking memory by not calling RemoveSourceChangedHandler.
                PresentationSource.AddSourceChangedHandler(child, OnSourceChanged);
            }
    
            void OnSourceChanged(object sender, SourceChangedEventArgs e)
            {
                HwndSource oldSource = e.OldSource as HwndSource;
                HwndSource newSource = e.NewSource as HwndSource;
                if (oldSource != null)
                {
                    oldSource.RemoveHook(SourceHook);
                }
                if (newSource != null)
                {
                    newSource.AddHook(SourceHook);
                }
            }
    
            IntPtr SourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
            {
                // Handle WM_GETDLGCODE in order to allow for arrow and tab navigation inside the dialog page.
                // By returning this code, Windows will pass arrow and tab keys to our HWND instead of handling
                // them for its own default tab and directional navigation.
                switch (msg)
                {
                    case NativeMethods.WM_GETDLGCODE:
                        int dlgCode = NativeMethods.DLGC_WANTARROWS | NativeMethods.DLGC_WANTTAB | NativeMethods.DLGC_WANTCHARS;
    
                        // Ask the currently-focused element if it wants to handle all keys or not.  The DialogKeyPendingEvent
                        // is a routed event starting with the focused control.  If any control in the route handles
                        // this message, then we'll add DLGC_WANTALLKEYS to request that this pending message
                        // be delivered to our content instead of the default dialog procedure.
                        IInputElement currentElement = Keyboard.FocusedElement;
                        if (currentElement != null)
                        {
                            DialogKeyEventArgs args = new DialogKeyEventArgs(DialogKeyPendingEvent, KeyInterop.KeyFromVirtualKey(wParam.ToInt32()));
                            currentElement.RaiseEvent(args);
    
                            if (args.Handled)
                            {
                                dlgCode |= NativeMethods.DLGC_WANTALLKEYS;
                            }
                        }
    
                        handled = true;
                        return new IntPtr(dlgCode);
                }
    
                return IntPtr.Zero;
            }
    
            private static void HandleComboBoxDialogKey(object sender, DialogKeyEventArgs e)
            {
                // If the ComboBox is dropped down and Enter or Escape are pressed, we should
                // cancel or commit the selection change rather than allowing the default button
                // or cancel button to be invoked.
                ComboBox comboBox = (ComboBox)sender;
                if ((e.Key == Key.Enter || e.Key == Key.Escape) && comboBox.IsDropDownOpen)
                {
                    e.Handled = true;
                }
            }
    
            private static void HandleDatePickerDialogKey(object sender, DialogKeyEventArgs e)
            {
                // If the DatePicker is dropped down and Enter or Escape are pressed, we should
                // cancel or commit the selection change rather than allowing the default button
                // or cancel button to be invoked.
                DatePicker datePicker = (DatePicker)sender;
                if ((e.Key == Key.Enter || e.Key == Key.Escape) && datePicker.IsDropDownOpen)
                {
                    e.Handled = true;
                }
            }
    
            /// <summary>
            /// Subclass of ElementHost designed to work around focus problems with ElementHost.
            /// </summary>
            class DialogPageElementHost : ElementHost
            {
                protected override void WndProc(ref Forms.Message m)
                {
                    base.WndProc(ref m);
    
                    if (m.Msg == NativeMethods.WM_SETFOCUS)
                    {
                        IntPtr oldHandle = m.WParam;
    
                        // Get the handle to the child WPF element that we are hosting
                        // After that get the next and previous items that would fall before 
                        // and after the WPF control in the tools->options page tabbing order
                        HwndSource source = PresentationSource.FromVisual(Child) as HwndSource;
                        if (source != null && oldHandle != IntPtr.Zero)
                        {
                            IntPtr nextTabElement = GetNextFocusElement(source.Handle, forward: true);
                            IntPtr previousTabElement = GetNextFocusElement(source.Handle, forward: false);
    
                            UIElement rootElement = source.RootVisual as UIElement;
    
                            // If we tabbed back from the next element then set focus to the last item
                            if (rootElement != null && nextTabElement == oldHandle)
                            {
                                rootElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.Last));
                            }
    
                            // If we tabbed in from the previous element then set focus to the first item
                            else if (rootElement != null && previousTabElement == oldHandle)
                            {
                                rootElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));
                            }
                        }
                    }
                }
    
                protected override void OnHandleCreated(EventArgs e)
                {
                    base.OnHandleCreated(e);
    
                    // Set up an IKeyboardInputSite that understands how to tab outside the WPF content.
                    // (see the notes on DialogKeyboardInputSite for more detail).
                    // NOTE: This should be done after calling base.OnHandleCreated, which is where
                    // ElementHost sets up its own IKeyboardInputSite.
                    HwndSource source = PresentationSource.FromVisual(Child) as HwndSource;
                    if (source != null)
                    {
                        ((IKeyboardInputSink)source).KeyboardInputSite = new DialogKeyboardInputSite(source);
                    }
                }
    
                // From a given handle get the next focus element either forward or backward
                internal static IntPtr GetNextFocusElement(IntPtr handle, bool forward)
                {
                    IntPtr hDlg = NativeMethods.GetAncestor(handle, NativeMethods.GA_ROOT);
                    if (hDlg != IntPtr.Zero)
                    {
                        // Find the next dialog item in the parent dialog (searching in the correct direction)
                        // This can return IntPtr.Zero if there are no more items in that direction
                        return NativeMethods.GetNextDlgTabItem(hDlg, handle, !forward);
                    }
    
                    return IntPtr.Zero;
                }
            }
    
            /// <summary>
            /// The default IKeyboardInputSite that ElementHost uses relies on being hosted
            /// in a pure Windows Forms window for tabbing outside the ElementHost's WPF content.
            /// However, this DialogPageElementHost is hosted inside a Win32 dialog, and should
            /// rely on the Win32 navigation logic directly.  This replaces the default
            /// IKeyboardInputSite with one that has specialized handling for OnNoMoreTabStops.
            /// </summary>
            class DialogKeyboardInputSite : IKeyboardInputSite
            {
                HwndSource _source;
                public DialogKeyboardInputSite(HwndSource source)
                {
                    _source = source;
                }
    
                /// <summary>
                /// Gets the IKeyboardInputSink associated with this site.
                /// </summary>
                public IKeyboardInputSink Sink
                {
                    get
                    {
                        return _source;
                    }
                }
    
                public void Unregister()
                {
                    // We have nothing to unregister, so do nothing.
                }
    
                public bool OnNoMoreTabStops(TraversalRequest request)
                {
                    // First, determine if we are tabbing forward or backwards
                    // outside of our content.
                    bool forward = true;
                    if (request != null)
                    {
                        switch (request.FocusNavigationDirection)
                        {
                            case FocusNavigationDirection.Next:
                            case FocusNavigationDirection.Right:
                            case FocusNavigationDirection.Down:
                                forward = true;
                                break;
    
                            case FocusNavigationDirection.Previous:
                            case FocusNavigationDirection.Left:
                            case FocusNavigationDirection.Up:
                                forward = false;
                                break;
                        }
                    }
    
                    // Based on the direction, tab forward or backwards in our parent dialog.
                    IntPtr nextHandle = DialogPageElementHost.GetNextFocusElement(_source.Handle, forward);
                    if (nextHandle != IntPtr.Zero)
                    {
                        // If we were able to find another control, send focus to it and inform
                        // WPF that we moved focus outside the HwndSource.
                        NativeMethods.SetFocus(nextHandle);
                        return true;
                    }
    
                    // If we couldn't find a dialog item to focus, inform WPF that it should
                    // continue cycling inside its own tab order.
                    return false;
                }
            }
        }
    }
    
    using System;
    using System.Runtime.InteropServices;
    
    namespace <your namespace here>
    {
        internal static class NativeMethods
        {
            public const int WM_GETDLGCODE = 0x0087;
            public const int WM_SETFOCUS = 0x0007;
    
            public const int DLGC_WANTARROWS = 0x0001;
            public const int DLGC_WANTTAB = 0x0002;
            public const int DLGC_WANTCHARS = 0x0080;
            public const int DLGC_WANTALLKEYS = 0x0004;
    
            public const int GA_ROOT = 2;
    
            [DllImport("user32.dll", ExactSpelling = true)]
            internal static extern IntPtr GetAncestor(IntPtr hWnd, int flags);
    
            [DllImport("user32.dll", ExactSpelling = true)]
            internal static extern IntPtr GetNextDlgTabItem(IntPtr hDlg, IntPtr hCtl, [MarshalAs(UnmanagedType.Bool)] bool bPrevious);
    
            [DllImport("user32.dll")]
            public static extern void SetFocus(IntPtr hwnd);
        }
    }
    


    • Proposed as answer by Yann Duran Friday, January 11, 2013 3:49 AM
    Wednesday, January 9, 2013 11:08 PM
    Moderator
  • Thank you Ryan!

    I'll give it a try & let you know how I get on.


    Yann - LightSwitch Central - Click here for FREE Themes, Controls, Types and Commands
     
    Please click "Mark as Answer" if a reply answers your question. Please click "Vote as Helpful" , if you find a reply helpful.
     
    By doing this you'll help others to find answers faster.

    Thursday, January 10, 2013 4:54 AM
  • Ryan,

    THANK YOU so much! That works brilliantly (as of course you knew it would).

    I now have perfectly functioning WPF options pages. :-)

    Yann


    Yann - LightSwitch Central - Click here for FREE Themes, Controls, Types and Commands
     
    Please click "Mark as Answer" if a reply answers your question. Please click "Vote as Helpful" , if you find a reply helpful.
     
    By doing this you'll help others to find answers faster.

    Friday, January 11, 2013 3:49 AM