我有一个WPF / MVVM应用程序,它包含一个带有几个按钮的窗口 每个按钮都会触发对外部设备(USB missile launcher)的调用,这需要几秒钟。
当设备运行时,GUI被冻结 (这没关系,因为应用程序的唯一目的是调用USB设备,而且当设备移动时你无法做任何其他事情!) < / p>
唯一有点难看的是,冻结的GUI在设备移动时仍会接受额外的点击 当设备仍然移动并且我再次点击同一个按钮时,设备会在第一次“运行”完成后立即再次开始移动。
所以我想在单击一个按钮后立即禁用GUI中的所有按钮,并在按钮的命令运行完毕后再次启用它们。
我找到了一个符合MVVM标准的解决方案 (至少对我来说......请注意我还是WPF / MVVM的初学者!)
问题是当我调用与USB设备通信的外部库时,此解决方案不起作用(如:禁用按钮)。
但是,禁用GUI的实际代码是正确的,因为当我用MessageBox.Show()
替换外部库调用时 工作。
我构建了一个最小的工作示例来重现问题(complete demo project here):
这是观点:
<Window x:Class="WpfDatabindingQuestion.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<Button Content="MessageBox" Command="{Binding MessageCommand}" Height="50"></Button>
<Button Content="Simulate external device" Command="{Binding DeviceCommand}" Height="50" Margin="0 10"></Button>
</StackPanel>
</Grid>
</Window>
...这是ViewModel (使用RelayCommand
from Josh Smith's MSDN article):
using System.Threading;
using System.Windows;
using System.Windows.Input;
namespace WpfDatabindingQuestion
{
public class MainWindowViewModel
{
private bool disableGui;
public ICommand MessageCommand
{
get
{
return new RelayCommand(this.ShowMessage, this.IsGuiEnabled);
}
}
public ICommand DeviceCommand
{
get
{
return new RelayCommand(this.CallExternalDevice, this.IsGuiEnabled);
}
}
// here, the buttons are disabled while the MessageBox is open
private void ShowMessage(object obj)
{
this.disableGui = true;
MessageBox.Show("test");
this.disableGui = false;
}
// here, the buttons are NOT disabled while the app pauses
private void CallExternalDevice(object obj)
{
this.disableGui = true;
// simulate call to external device (USB missile launcher),
// which takes a few seconds and pauses the app
Thread.Sleep(3000);
this.disableGui = false;
}
private bool IsGuiEnabled(object obj)
{
return !this.disableGui;
}
}
}
我怀疑打开MessageBox
会触发后台中的某些内容,当我只是调用外部库时会发生不。
但我无法找到解决方案。
我也尝试过:
INotifyPropertyChanged
(并将this.disableGui
设为属性,并在更改时调用OnPropertyChanged
)CommandManager.InvalidateRequerySuggested()
(我在SO上找到类似问题的几个答案) 有什么建议吗?
答案 0 :(得分:10)
试试这个:
//Declare a new BackgroundWorker
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += (o, ea) =>
{
try
{
// Call your device
// If ou need to interact with the main thread
Application.Current.Dispatcher.Invoke(new Action(() => //your action));
}
catch (Exception exp)
{
}
};
//This event is raise on DoWork complete
worker.RunWorkerCompleted += (o, ea) =>
{
//Work to do after the long process
disableGui = false;
};
disableGui = true;
//Launch you worker
worker.RunWorkerAsync();
答案 1 :(得分:9)
好的CanExecute
方法无效,因为点击会立即让您进入长时间运行的任务。
所以我会这样做:
使您的视图模型实现INotifyPropertyChanged
添加名为:
的属性public bool IsBusy
{
get
{
return this.isBusy;
}
set
{
this.isBusy = value;
RaisePropertyChanged("IsBusy");
}
}
以这种方式将按钮绑定到此属性:
<Button IsEnabled="{Binding IsBusy}" .. />
在ShowMessage / CallExternal设备方法中添加行
IsBusy = true;
应该做的伎俩
答案 2 :(得分:5)
因为您在主线程上运行CallExternalDevice()
,所以在该作业完成之前,主线程将没有时间更新任何UI,这就是按钮保持启用的原因。您可以在单独的线程中启动长时间运行的操作,您应该看到按钮被按预期禁用:
private void CallExternalDevice(object obj)
{
this.disableGui = true;
ThreadStart work = () =>
{
// simulate call to external device (USB missile launcher),
// which takes a few seconds and pauses the app
Thread.Sleep(3000);
this.disableGui = false;
Application.Current.Dispatcher.BeginInvoke(new Action(() => CommandManager.InvalidateRequerySuggested()));
};
new Thread(work).Start();
}
答案 3 :(得分:5)
我认为这更优雅:
XAML:
<Button IsEnabled="{Binding IsGuiEnabled}" Content="Simulate external device" Command="{Binding DeviceCommand}" Height="50" Margin="0 10"></Button>
C#(使用async&amp; await):
public class MainWindowViewModel : INotifyPropertyChanged
{
private bool isGuiEnabled;
/// <summary>
/// True to enable buttons, false to disable buttons.
/// </summary>
public bool IsGuiEnabled
{
get
{
return isGuiEnabled;
}
set
{
isGuiEnabled = value;
OnPropertyChanged("IsGuiEnabled");
}
}
public ICommand DeviceCommand
{
get
{
return new RelayCommand(this.CallExternalDevice, this.IsGuiEnabled);
}
}
private async void CallExternalDevice(object obj)
{
IsGuiEnabled = false;
try
{
await Task.Factory.StartNew(() => Thread.Sleep(3000));
}
finally
{
IsGuiEnabled = true;
}
}
}