0

I have a button in my WPF application, on clicking it a new process is started. I want to keep the button disabled until the process is completed. Here I use BeginInvoke() expecting the changes in IsEnabled will be reflected in UI. However the button is always in enabled state.

MainWindow.xaml

<Window x:Class="ButtonTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ButtonTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Button Width="100" Height="30" Content="Start" IsEnabled="{Binding Path=IsEnabled}" Click="Button_Click"/>
    </Grid>
</Window>

MainWindowViewModel.cs

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void SendPropertyChangedEvent(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
class MainWindowViewModel : ViewModelBase
{
    private bool m_IsEnabled;

    public bool IsEnabled
    {
        get { return m_IsEnabled; }
        set
        {
            if (this.m_IsEnabled != value)
            {
                m_IsEnabled = value;
                this.SendPropertyChangedEvent(nameof(this.IsEnabled));
            }
        }
    }
}

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.DataContext = new MainWindowViewModel();
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        (DataContext as MainWindowViewModel).IsEnabled = false;

        App.Current.Dispatcher.BeginInvoke(new Action(() => {
            Correlate();
        }));
    }

    private void Correlate()
    {
        Process process = new Process();
        process.StartInfo.FileName = "test.exe";
        process.Start();
        process.WaitForExit();

        (DataContext as MainWindowViewModel).IsEnabled = true; 
    }
}
FaisalM
  • 724
  • 5
  • 18
  • Instead of setting this.IsEnabled = false; replace this to (DataContext as MainWindowViewModel).IsEnabled = false; – mohammed mazin Jan 20 '20 at 09:33
  • Corrected to use the `IsEnabled` form `ViewModel`. Same behavior. – FaisalM Jan 20 '20 at 09:54
  • Never use the `as` operator without checking the result for `null`. Use an explicit cast instead: `((MainWindowViewModel)DataContext).IsEnabled = false;`. In case the DataContext is for whatever reason not a MainWindowViewModel instance, you would correctly get an InvalidCastException instead of an incorrect NullReferenceException. – Clemens Jan 20 '20 at 09:56

1 Answers1

2

Even with BeginInvoke, the Correlate method still runs in the UI thread and blocks it.

Use an awaited Task instead:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var vm = (MainWindowViewModel)DataContext;
    vm.IsEnabled = false;

    await Correlate();

    vm.IsEnabled = true;
}

private Task Correlate()
{
    return Task.Run(() =>
    {
        var process = new Process();
        process.StartInfo.FileName = "test.exe";
        process.Start();
        process.WaitForExit();
    });
}

Or use the WaitForExitAsync method shown here:

private async Task Correlate() 
{
    var process = new Process();
    process.StartInfo.FileName = "test.exe";
    process.Start();
    await process.WaitForExitAsync();
}

You may also not need the IsEnabled property in the view model. You could as well directly set the Button's property:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var button = (Button)sender;
    button.IsEnabled = false;

    await Correlate();

    button.IsEnabled = true;
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • 2
    I am okay with UI remains blocked. I just want to disable the button while the process is executing. – FaisalM Jan 20 '20 at 09:52
  • But using 'BeginInvoke` the UI thread get a chance to update the button status? right? I thought `BeginInvoke` works in same line as Win32 API `PostMessage`. – FaisalM Jan 20 '20 at 09:56
  • What if you directly set the Button's IsEnabled property? The IsEnabled property in the view model seems redundant. – Clemens Jan 20 '20 at 09:58
  • And I still greatly dislike the idea of a blocked UI as long as the process is running. There is no reason at all to have such an awkward behaviour. – Clemens Jan 20 '20 at 09:59
  • I tried setting button's `IsEnabled`property directly. Still the same behavior. – FaisalM Jan 20 '20 at 10:01
  • I agree having blocked UI is a bad design. I am trying to understand how `BeginInvoke` behaves. – FaisalM Jan 20 '20 at 10:03
  • 1
    Try a lower DispatcherPriority. – Clemens Jan 20 '20 at 10:06
  • Using `DispatcherPriority.ApplicationIdle` the button is disabling. – FaisalM Jan 20 '20 at 10:11