dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.08k stars 1.17k forks source link

[API Proposal]: Make Help button available in MessageBox #9619

Open bstordrup opened 2 months ago

bstordrup commented 2 months ago

Background and motivation

Split from the API Proposal in #9613.

The MessageBox API in Windows has an ability to make a Help button available in the UI. This involves the MB_HELP value in the style for the MessageBox call.

Doing so, developers will be able to integrate help into messages being shown making the end users able to read more information about a message.

API Proposal

The proposal is to add help related parameters to MessageBox.Show method in the same way as in WinForms repository.

It would be something like this:

namespace System.Windows 
{
    class MessageBox
    {
        public static MessageBoxResult Show(
            string messageBoxText,
            string caption,
            MessageBoxButton button,
            MessageBoxImage icon, 
            MessageBoxResult defaultResult,
            MessageBoxOptions options,
+           bool displayHelpButton);

        public static MessageBoxResult Show(
            string messageBoxText,
            string caption,
            MessageBoxButton button,
            MessageBoxImage icon, 
            MessageBoxResult defaultResult,
            MessageBoxOptions options,
+           string helpFilePath);

        public static MessageBoxResult Show(
            Window owner,
            string messageBoxText,
            string caption,
            MessageBoxButton button,
            MessageBoxImage icon, 
            MessageBoxResult defaultResult,
            MessageBoxOptions options,
+           string helpFilePath);

        public static MessageBoxResult Show(
            string messageBoxText,
            string caption,
            MessageBoxButton button,
            MessageBoxImage icon, 
            MessageBoxResult defaultResult,
            MessageBoxOptions options,
+           string helpFilePath,
+           string keyWord);

        public static MessageBoxResult Show(
            Window owner,
            string messageBoxText,
            string caption,
            MessageBoxButton button,
            MessageBoxImage icon, 
            MessageBoxResult defaultResult,
            MessageBoxOptions options,
+           string helpFilePath,
+           string keyWord);

    }
}

The implementation in WinForms repository will be a source of inspiration for implementing this.

Also, the underlying Windows API has a value for a default button 4. It can be made available also like this:

namespace System.Windows
{
    public sealed class MessageBox
    {
        private const int DEFAULT_BUTTON1 = 0x00000000;
        private const int DEFAULT_BUTTON2 = 0x00000100;
        private const int DEFAULT_BUTTON3 = 0x00000200;
+       private const int DEFAULT_BUTTON4 = 0x00000400;
    }
}

but this will more be for consistency with the underlying API. The value is not being used in the implementation that calculates a default button number based on the MessageBoxResult defaultResult.

If the DEFAULT_BUTTON4 should be usefull, it would mean to make a new enum for this and changing all calls to MessageBox.Show to allow the caller to specify a default button instead of a default result. This would look like this:

namespace System.Windows
{
    public enum MessageBoxDefaultBotton
    {
        Button1 = 0x00000000,
        Button2 = 0x00000100,
        Button3 = 0x00000200,
        Button4 = 0x00000400
    }

    public sealed class MessageBox
    {
        public static MessageBoxResult Show(
            string messageBoxText,
            string caption,
            MessageBoxButton button,
            MessageBoxImage icon, 
-           MessageBoxResult defaultResult,
+           MessageBoxDefaultButton defaultButton,
            MessageBoxOptions options,
+           bool displayHelpButton);

}

API Usage

// Fancy the value
var msgBoxResult = System.Windows.MessageBox.Show("My short message", 
    "My app name",
    MessageBoxButton.OkCancel,
    MessageBoxImage.Information,
    MessageBoxResult.Ok,  // Or MessageBoxDefaultButton.Button1
    0, 
    "<Path to my help file>",
    "<Help key word to a more detailed message>");

This will open the help file specified on the key work specified

Alternative Designs

No response

Risks

It must be ensured that a valid handle for sending the WM_HELP message is found (1). This will have to be a part of the ShowCore changes. The implementation in WinForms repository handles it by getting the Active Window if an owner is not specified.

  1. https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
lindexi commented 2 months ago

The proposal is very attractive.

miloush commented 2 months ago

Yeah as I probably gave away, I am not very keen on this API. How does the displayHelpButton allow developer to interact with it? What are the expected values for the strings, file paths to .chm? web links? anything to shell-execute? Can you point to the Winforms PR implementing this?

Could we not have some Help event instead that would get called when the button is clicked?

bstordrup commented 2 months ago

displayHelpButton makes the MessageBox Windows API put a Help button in the MessageBox. And it will then send a WM_HELP message to the owner [1], in the MB_HELP description.

This means that the developer need listen for the WM_HELP message in his own application and activate the relevant help items.

In WinForms repository, this is implemented using an overload of ShowCore overloads that takes a HelpInfo instance as parameter:

    private static DialogResult ShowCore(
        IWin32Window? owner,
        string? text,
        string? caption,
        MessageBoxButtons buttons,
        MessageBoxIcon icon,
        MessageBoxDefaultButton defaultButton,
        MessageBoxOptions options,
        HelpInfo hpi)
    {
        DialogResult result = DialogResult.None;
        try
        {
            PushHelpInfo(hpi);
            result = ShowCore(owner, text, caption, buttons, icon, defaultButton, options, true);
        }
        finally
        {
            PopHelpInfo();
        }

        return result;
    }

The other ShowCore surrounds the call to the Windows API MessageBox API call with a modal message loop:

        Application.BeginModalMessageLoop();
        try
        {
            return (DialogResult)PInvoke.MessageBox(handle.Handle, text, caption, style);
        }
        finally
        {
            Application.EndModalMessageLoop();

            // Right after the dialog box is closed, Windows sends WM_SETFOCUS back to the previously active control
            // but since we have disabled this thread main window the message is lost. So we have to send it again after
            // we enable the main window.
            PInvoke.SendMessage(handle, PInvoke.WM_SETFOCUS);
        }

The methods PushHelpInfo and PopHelpInfo maintains the contents of a thread static HelpInfo[]? array, and MessageBox class exposes a static HelpInfo? HelpInfo read only property that will return the first element in the HelpInfo[]? array or null if the array is null or does not have any items.

This HelpInfo instance can then be retrieved in the callback for WM_HELP message and used to activate the help system.

  1. https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
miloush commented 2 months ago

So then Help button can be just another button or part of the dialog options, and Window can have a sort of Help event, where the developer can do whatever they want.

h3xds1nz commented 2 months ago

You cannot ask people to listen for WM messages for such a simple thing, you will need to abstract it with an event.

bstordrup commented 2 months ago

You cannot ask people to listen for WM messages for such a simple thing, you will need to abstract it with an event.

In WinForms repository, this is handled on the Control class with this method:

    /// <summary>
    ///  Handles the WM_HELP message
    /// </summary>
    private unsafe void WmHelp(ref Message m)
    {
        // If there's currently a message box open - grab the help info from it.
        HelpInfo? hpi = MessageBox.HelpInfo;
        if (hpi is not null)
        {
            switch (hpi.Option)
            {
                case HelpInfo.HelpFileOption:
                    Help.ShowHelp(this, hpi.HelpFilePath);
                    break;
                case HelpInfo.HelpKeywordOption:
                    Help.ShowHelp(this, hpi.HelpFilePath, hpi.Keyword);
                    break;
                case HelpInfo.HelpNavigatorOption:
                    Help.ShowHelp(this, hpi.HelpFilePath, hpi.Navigator);
                    break;
                case HelpInfo.HelpObjectOption:
                    Help.ShowHelp(this, hpi.HelpFilePath, hpi.Navigator, hpi.Param);
                    break;
            }
        }

        // Note: info.hItemHandle is the handle of the window that sent the help message.
        HELPINFO* info = (HELPINFO*)(nint)m.LParamInternal;
        HelpEventArgs hevent = new(info->MousePos);
        OnHelpRequested(hevent);
        if (!hevent.Handled)
        {
            DefWndProc(ref m);
        }
    }

It is being called from the WndProc method on Control class - which is the place where Windows Messages are handled:

            case PInvoke.WM_HELP:
                WmHelp(ref m);
                break;

The code shows that the WM_HELP will trigger the OnHelpRequested event on the Control class.

I think a similar thing can be done on the Window class in Wpf by extending the WindowFilterMessage and responding on the WM_HELP message in a similar way.

bstordrup commented 2 months ago

It might also be that a HelpRequested should be added as a RoutedEventHandler in UIElement class and then triggered on the uiElement in either HwndSource or HwndTarget classes somewhere. Would need to experiment a little with how the WM_HELP message could be picked up.

miloush commented 2 months ago

It should be added on Window class because Window is the only allowed owner of a MessageBox. Also it could be used for help button on the window itself.

bstordrup commented 2 months ago

@miloush, is it the HelpRequested event, you mean?

In WinForms, that event is located on Control class, which allows for the individual controls to respond to the event and provide specific help for that control.

If the HelpRequested is only available on Window class, then the window need to figure out where the user was requesting the help from and actively delegate the request there.

miloush commented 2 months ago

In Win32/Winforms, every control has a hwnd, which is not the case in WPF. But the event provides mouse coordinates, so it could be hit-tested to an individual element/visual. It can also apparently occur for a menu item.

An alternative way is to use the ApplicationCommands.Help command. I haven't really explored the idea very much, just thinking out loud. But I don't like WPF trying to execute things automatically somehow, we don't even navigate Hyperlinks...

bstordrup commented 2 months ago

OK. Good input - will try to experiment with it and see if I can get something working.