0

I have a panel with a button on it that is used to trigger an image capture from an external camera. The capture can take several seconds, so I want the button to disable when capture is in progress. I also want to be able to prevent the user capturing when my program is running a control script. Here is my ViewModel class:

public class CameraControlViewModel : ViewModelBase
{
    public CameraControlViewModel()
    {

    }

    public CameraControlViewModel( DataModel dataModel )
    : base( dataModel )
    {
        dataModel.PropertyChanged += DataModelOnPropertyChanged;    

        _captureImageCommand = new RelayCommand( captureImage );
        _capturedImage = new BitmapImage();
        _capturedImage.BeginInit();
        _capturedImage.UriSource = new Uri( "Images/fingerprint.jpg", UriKind.Relative );
        _capturedImage.CacheOption = BitmapCacheOption.OnLoad;
        _capturedImage.EndInit();
    }

    public ICommand CaptureImageCommand
    {
        get { return _captureImageCommand; }
    }

    public bool CanCaptureImage
    {
        get { return !dataModel.IsScriptRunning && !_captureInProgress; }
    }

    public bool IsCaptureInProgress
    {
        get { return _captureInProgress; }
        set
        {
            if (_captureInProgress != value)
            {
                _captureInProgress = value;
                OnPropertyChanged( "IsCaptureInProgress" );
                OnPropertyChanged( "CanCaptureImage" );
            }
        }
    }

    public int PercentDone
    {
        get { return _percentDone; }
        set
        {
            if (_percentDone != value)
            {
                _percentDone = value;
                OnPropertyChanged( "PercentDone" );
            }
        }
    }

    public BitmapImage CapturedImage
    {
        get { return _capturedImage; }    
    }

    private void DataModelOnPropertyChanged( object sender, PropertyChangedEventArgs propertyChangedEventArgs )
    {
        string property = propertyChangedEventArgs.PropertyName;
        if (property == "IsScriptRunning")
        {
            OnPropertyChanged( "CanCaptureImage" );    
        }

        OnPropertyChanged( property );
    }

    private void captureImage( object arg )
    {
        IsCaptureInProgress = true;
        PercentDone = 0;

        // TODO: remove this placeholder.
        new FakeImageCapture( this );

        // TODO (!)    
    }

    internal void captureComplete()
    {
        IsCaptureInProgress = false;
    }

    // Remove this placeholder when we can take images.
    private class FakeImageCapture
    {
        CameraControlViewModel _viewModel;
        int _count;
        Timer _timer = new Timer();

        public FakeImageCapture( CameraControlViewModel viewModel )
        {
            this._viewModel = viewModel;

            _timer.Interval = 50;
            _timer.Elapsed += TimerOnTick;
            _timer.Start();
        }

        private void TimerOnTick( object sender, EventArgs eventArgs )
        {
            ++_count;
            if (_count <= 100)
            {
                _viewModel.PercentDone = _count;
            }
            else
            {
                Application.Current.Dispatcher.Invoke( (Action)_viewModel.captureComplete );
                _timer.Stop();
                _timer = null;
                _viewModel = null;
            }
        }
    }

    private readonly ICommand _captureImageCommand;
    private volatile bool _captureInProgress;
    private BitmapImage _capturedImage;
    private int _percentDone;
}

Here is the XAML for the button:

<Button Command="{Binding CaptureImageCommand}" 
        Grid.Row="0" Grid.Column="0" 
        Margin="4" 
        IsEnabled="{Binding CanCaptureImage}"
        ToolTip="Capture Image">
            <Image Source="../Images/camera-icon.gif" Width="64" Height="64" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Button>

Clicking the "capture" button goes fine. The button disables and elsewhere a progress bar appears showing the (currently faked) image capture progress. However, when the capture completes, even though I set the CanCaptureImage property in the captureComplete() method, the button does not change back to its "enabled" appearance. It will only do this when I click somewhere (anywhere) in the window. However, the button is actually enabled because I can click on it again to trigger a 2nd capture.

I have tried CommandManager.InvalidateRequerySuggested() inside captureComplete() but that doesn't help. Any ideas?

Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
Julian Gold
  • 1,246
  • 2
  • 19
  • 40
  • Are you sure the script isn't still running? – Emond Jul 02 '13 at 10:26
  • Absolutely, @ErnodeWeerd. – Julian Gold Jul 02 '13 at 10:29
  • When you set a break point on the CanCaptureImage getter, is it evaluated after the capture has finished? There has to be a signal to the UI that the CanCaptureImage should be reevaluated... – Emond Jul 02 '13 at 10:32
  • 1
    Maybe you can try raising ICommand.CanExecuteChanged event of the CaptureImageCommand, in the IsCaptureInProgress property setter after you change the value. – Jurica Smircic Jul 02 '13 at 10:35
  • @ErnodeWeerd yes, that property is evaluating correctly. But still the button does not refresh till I click elsewhere. I thought it might have to do with my button styling, but I turned all that off and it still happens with default visuals. – Julian Gold Jul 02 '13 at 10:53
  • Have a look at this: http://stackoverflow.com/questions/1340302/wpf-how-to-force-a-command-to-re-evaluate-canexecute-via-its-commandbindings – Emond Jul 02 '13 at 11:12
  • @ErnodeWeerd after a bit of moving the CommandManager.InvalidateRequerySuggested() call around, and having tweaked some other bits and pieces, it now appears to work. It all feels a bit unsatisfactory, really. – Julian Gold Jul 02 '13 at 11:38
  • "new FakeImageCapture( this );" you compiler should come screaming at you with at least one warning. You may want to fix that first. Currently, I see no reason why that instance should not be garbage collected the moment the constructor is done. – nvoigt Jul 02 '13 at 11:38
  • @nvoigt since I see my progress bar go up, one presumes the GC finds a good reason to hold on to the instance. Then again, I got into the habit of doing this sort of hack in Java where inner classes behave quite differently :) – Julian Gold Jul 02 '13 at 11:44
  • This issue is that WPF/the UI does not know when to re-evaluate the CanExecute properties. This is why you might need to force a re-evaluation. – Emond Jul 02 '13 at 12:47

1 Answers1

1

Rather than having a separate IsEnabled binding to enable/disable the button, you should really just use the CanExecute predicate of the RelayCommand: http://msdn.microsoft.com/en-us/library/hh727783.aspx

This would ensure that the button will get enabled/disabled properly when calling CommandManager.InvalidateRequerySuggested(). Get rid of the CanCaptureImage property and modify your code as follows:

public CameraControlViewModel( DataModel dataModel )
: base( dataModel )
{
    dataModel.PropertyChanged += DataModelOnPropertyChanged;    

    _captureImageCommand = new RelayCommand( captureImage, captureImage_CanExecute );
    _capturedImage = new BitmapImage();
    _capturedImage.BeginInit();
    _capturedImage.UriSource = new Uri( "Images/fingerprint.jpg", UriKind.Relative );
    _capturedImage.CacheOption = BitmapCacheOption.OnLoad;
    _capturedImage.EndInit();
}

private bool captureImage_CanExecute( object arg)
{
    return !dataModel.IsScriptRunning && !_captureInProgress;
}
17 of 26
  • 27,121
  • 13
  • 66
  • 85
  • I think this is what got things working. Instead of doing exactly what you did, I added the canExecute predicate, and made that return CanCaptureImage. Six, half dozen. Thanks! – Julian Gold Jul 02 '13 at 16:33