Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF ContextMenu still visible after launching an external process

I'm launching an external application from a ContextMenu, and I must block the the source application while the target application is running. To achieve this I'm using Process.WaitForExit() to avoid the source application responding to events.

The problem is the context menu is still ahead the target application. Let's see it with a simple example:

enter image description here

This is the code I'm using for the example.

    public MainWindow()
    {
        InitializeComponent();

        this.ContextMenu = new ContextMenu();

        MenuItem menuItem1 = new MenuItem();
        menuItem1.Header = "Launch notepad";
        menuItem1.Click += MyMenuItem_Click;
        this.ContextMenu.Items.Add(menuItem1);
    }

    void MyMenuItem_Click(object sender, RoutedEventArgs e)
    {
        Process p = new Process();
        p.StartInfo.FileName = "notepad.exe";
        p.StartInfo.CreateNoWindow = false;
        p.Start();
        p.WaitForExit();
        p.Close();
    }

How could I make the ContextMenu to disappear before the target application is displayed?

like image 609
Daniel Peñalba Avatar asked Mar 31 '16 14:03

Daniel Peñalba


4 Answers

One possible solution is to start process when menu is closed:

bool _start;

public MainWindow()
{
    InitializeComponent();

    ContextMenu = new ContextMenu();
    var menuItem = new MenuItem() { Header = "Launch notepad" };
    menuItem.Click += (s, e) => _start = true;
    ContextMenu.Items.Add(menuItem);
    ContextMenu.Closed += (s, e) =>
    {
        if (_start)
        {
            _start = false;
            using (var process = new Process() { StartInfo = new ProcessStartInfo("notepad.exe") { CreateNoWindow = false } })
            {
                process.Start();
                process.WaitForExit();
            }
        }
    };
}

Tested and seems to work, but I doubt if idea with blocking UI thread for a long time is a good idea.

like image 117
Sinatr Avatar answered Nov 16 '22 14:11

Sinatr


After some tests, this seems to work ok:

void LaunchAndWaitForProcess(object sender, RoutedEventArgs routedEventArgs)
{
    this.ContextMenu.Closed -= LaunchAndWaitForProcess;
    Process p = new Process();
    p.StartInfo.FileName = "notepad.exe";
    p.StartInfo.CreateNoWindow = false;
    p.Start();
    p.WaitForExit();
    p.Close();
}
void MyMenuItem_Click(object sender, RoutedEventArgs e)
{
    this.ContextMenu.Closed += LaunchAndWaitForProcess;
}

Since (in the comments) you say you are using ICommands, I guess you are binding them in XAML and don't want to lose that.

A general-purpose way (ugly, and won't allow you to have CanExecute bound to the Enabled state of the menuitem, but the least ugly I could figure out in so little time) could be binding them to another property (for example: Tag), while also binding to a single Click handler. Something like:

In the XAML:

<MenuItem Header="Whatever" Tag="{Binding MyCommand}" Click="MenuItemsClick"         
<MenuItem Header="Other Item" Tag="{Binding OtherCommand}" Click="MenuItemsClick" />

In the code-behind:

private ICommand _launchCommand;
private object _launchCommandParameter;

void ExecuteContextMenuCommand(object sender, RoutedEventArgs routedEventArgs)
{
   this.ContextMenu.Closed -= ExecuteContextMenuCommand;
   if(_launchCommand != null && _launchCommand.CanExecute(_launchCommandParameter))
     _launchCommand.Execute(_launchCommandParameter);
}

void MenuItemsClick(object sender, RoutedEventArgs e)
{
    var mi = (sender as MenuItem);
    if(mi == null) return;
    var command = mi.Tag as ICommand;
    if(command == null) return;
    _launchCommand = command;
    _launchCommandParameter = mi.CommandParameter;
    this.ContextMenu.Closed += ExecuteContextMenuCommand;
}
like image 38
Jcl Avatar answered Nov 16 '22 14:11

Jcl


Blocking the UI thread (drawing, Window interaction) makes horrible UX: it looks like the application is frozen, which it actually is. I would go like this given the constraints:

void MyMenuItem_Click(object sender, RoutedEventArgs e) {
    Process p = new Process();
    p.StartInfo.FileName = "notepad.exe";
    p.StartInfo.CreateNoWindow = false;
    Title = "Waiting for Notepad to be closed.";
    executeBlocking(p, () => {
        Title = "Finished work with Notepad, you may resume your work.";
    });
}

void executeBlocking(Process p, Action onFinish) {
    IsEnabled = false;
    BackgroundWorker processHandler = new BackgroundWorker();
    processHandler.DoWork += (sender, e) => {
        p.Start();
        p.WaitForExit(); // block in background
        p.Close();
    };
    processHandler.RunWorkerCompleted += (sender, e) => {
        IsEnabled = true;
        onFinish.Invoke();
    };
    processHandler.RunWorkerAsync();
}

Disabling the Window itself (IsEnabled = false) will achieve "I must block the source application" by not letting the user interact with your app, other than Moving, Resizing and Closing it. If you need to block exiting as well you can do so like this:

InitializeComponent();
Closing += (sender, e) => {
    if (!IsEnabled) {
        MessageBox.Show("Sorry, you must close Notepad to exit the application");
        e.Cancel = true;
    }
};

It should be also general courtesy to the user to indicate you're waiting for notepad and once that's finished (closed) your app will be usable again, I did this in the window Title for simplicity and due to lack of any controls in the demo app.

like image 1
TWiStErRob Avatar answered Nov 16 '22 15:11

TWiStErRob


Based on the great ideas given by Jcl...

I found a simple solution using custom MenuItems for the menu. It delays the MenuItem.Click() event until the parent ContextMenu is closed.

class MyMenuItem : MenuItem
{
    protected override void OnClick()
    {
        ContextMenu parentContextMenu = Parent as ContextMenu;
        parentContextMenu.Closed += ContextMenu_Closed;
        parentContextMenu.IsOpen = false;
    }

    void ContextMenu_Closed(object sender, RoutedEventArgs e)
    {
        ContextMenu parent = Parent as ContextMenu;
        parent.Closed -= ContextMenu_Closed;
        base.OnClick();
    }
}
like image 1
Daniel Peñalba Avatar answered Nov 16 '22 13:11

Daniel Peñalba