可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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:
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?
回答1:
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.
回答2:
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 ICommand
s, 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;
}
回答3:
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.
回答4:
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();
}
}
回答5:
Note: This won't block the main application so if that is necessary, this answer won't work for you
The ui thread is being blocked so it can't undraw the context menu. Why you can see it over other programs likely has to do with how it is drawn on the screen. As noted in the Process.WaitForExit docs
WaitForExit() makes the current thread wait until the associated
process terminates. It should be called after all other methods are
called on the process. To avoid blocking the current thread, use the
Exited event.
So you need to change your code to this
void MyMenuItem_Click(object sender, RoutedEventArgs e)
{
Process p = new Process();
p.StartInfo.FileName = "notepad.exe";
p.StartInfo.CreateNoWindow = false;
p.EnableRaisingEvents = true;
p.Exited += new EventHandler(myProcess_Exited);
p.Start();
}
and then
private void myProcess_Exited(object sender, System.EventArgs e)
{
//Do whatever logic you have for when the program exits
}