kpischi
kpischi

Reputation: 35

Progressbar progress during long task

It's my first post here, so I hope I'm doing everything correct.

I'm using the .NET Framework 4 Client Profile.

I want to load data from a .doc file into my program and work with this information. This can take a lot of time since I need to run through the tables of the document and check what's inside. That is already working, the only problem here is the screen is freezing and you can't see if something is happening.

Also I know this would be faster and way easier in excel, but since this type of data is and was always stored in word-documents in our company I have to keep it like that.

So what I want to do is count all rows from the tables that I have to read, set this as my Maximum Value for the Progress-Bar and then after each row I would count the value + 1.

I have my load Button with the Command bound to LoadWordDocCmd and the progress bar:

<Button Name="btnLoadFile" 
        Content="Load" Height="23" 
        Command="{Binding LoadWordDocCmd}"
        HorizontalAlignment="Right" Margin="0,22,129,0" 
        VerticalAlignment="Top" Width="50" 
        Visibility="{Binding VisModeAddNew}"
        />

<ProgressBar HorizontalAlignment="Left" Height="24" Margin="574,52,0,0" 
             VerticalAlignment="Top" Width="306"
             Name="prgBarAddNewLoadWord"
             Minimum="0"
             Maximum="{Binding AddNewProgressBarMaxVal, Mode=OneWay}"
             Value="{Binding AddNewProgressBarValue, Mode=OneWay}"
             Visibility="{Binding AddNewProgressBarVisible}"/>

Here is the RelayCommand:

/// <summary>
/// Relaycommand for Function loadWordDocument
/// </summary>
public RelayCommand LoadWordDocCmd
{
  get
  {
    if (this.m_loadWordDocCmd == null)
    {
      this.m_loadWordDocCmd = new RelayCommand(this.loadWordDocument, canLoadWordDoc);
    }
    return m_loadWordDocCmd;
  }
  private set
  {
    this.m_loadWordDocCmd = value;
  }
}

/// <summary>
/// checks if the Word Document can be loaded
/// </summary>
/// <param name="parameter">not used</param>
/// <returns>if it could Execute, then true, else false</returns>
private bool canLoadWordDoc(object parameter)
{
  bool ret = false;

  if (this.m_fileSelected)
  {
    ret = true;
  }
  return ret;
}

What I already did was to work with a BackgroundWorker. I was able to bind the Button-Command to a function that has a RelayCommand with the BackgroundWorker, but then I wasn't able to check the canExecute function anymore.

I used this to test the Progress-Bar, that was working :

xaml:

  <Button   ...
            Command="{Binding Path=InstigateWorkCommand}" 
            />

cs :

     private BackgroundWorker worker; 
     private ICommand instigateWorkCommand;

     public ProggressbarSampleViewModel()
     {
        this.instigateWorkCommand = new 
                      RelayCommand(o => this.worker.RunWorkerAsync(), o => !this.worker.IsBusy);
        this.worker = new BackgroundWorker();
        this.worker.DoWork += this.DoWork;
        this.worker.ProgressChanged += this.ProgressChanged;
    }


    public ICommand InstigateWorkCommand
    {
        get { return this.instigateWorkCommand; }
    }

    private int _currentProgress;
    public int CurrentProgress
    {
        get { return this._currentProgress; }
        private set
        {
            if (this._currentProgress != value)
            {
                this._currentProgress = value;
                OnPropertyChanged("CurrentProgress"); 
            }
        }
     }

     private void ProgressChanged(object sender, ProgressChangedEventArgs e)
     {
        this.CurrentProgress = e.ProgressPercentage;
     }

    private void DoWork(object sender, DoWorkEventArgs e)
    {
        // do time-consuming work here, calling ReportProgress as and when you can   
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(1000);
            _currentProgress = i;
            OnPropertyChanged("CurrentProgress");
        }
    }

But how can I get this to work with the canExecute ? Here is my function-Header:

/// <summary>
/// Function for Load Word Document
/// </summary>
/// <param name="parameter">not used</param>
private void loadWordDocument(object parameter)

Here is the Relay-Command Class:

public class RelayCommand : ICommand
  {
    private readonly Action<object> methodToExecute;

    private readonly Func<object, bool> canExecute;

    public event EventHandler CanExecuteChanged;

    public void RaiseCanExecuteChanged()
    {
      EventHandler handler = CanExecuteChanged;
      if (handler != null)
      {
        handler(this, EventArgs.Empty);
      }
    }

    public RelayCommand(Action<object> execute)
      : this(execute, null) { }

    public RelayCommand(Action<object> methodToExecute, Func<object, bool> canExecute)
    {
      this.methodToExecute = methodToExecute;
      this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
      // wird keine canExecute-Funktion übergeben, so liefert diese
      // true zurück, ansonsten wird die custom canExecute-Funktion
      // mit den übergebenen Parametern aufgerufen.
      return canExecute == null ? true : canExecute.Invoke(parameter);
    }

    public void Execute(object parameter)
    {
      methodToExecute(parameter);
    }
  }

Thank you for your help and I hope I posted this question correct!

Upvotes: 1

Views: 1775

Answers (2)

MatrixRonny
MatrixRonny

Reputation: 781

I think that your ProggressbarSampleViewModel code sample is ok. I tested it and it works.

I am assuming that you want to change LoadWordDocCmd to have the behavior of InstigateWorkCommand. If you put the code from ProgressbarSampleViewModel into your actual ViewModel, you should have no problem accessing loadWordDocument and canLoadWordDoc. In addition, as mm8 mentioned, in your DoWork method you need to call RaiseCanExecuteChanged or else WPF will not check the CanExecute method.

Your ViewModel should look like bellow. See comments in upper case.

private BackgroundWorker worker;
private RelayCommand instigateWorkCommand;  //CHANGE HERE

bool isBusy = false;  // ADD THIS
public ProggressbarSampleViewModel()
{
    //CHANGE NEXT LINE
    this.instigateWorkCommand = new RelayCommand(
        o => this.worker.RunWorkerAsync(),
        o => !isBusy && canLoadWordDoc(null));
    this.worker = new BackgroundWorker();
    this.worker.DoWork += this.DoWork;

    //REMOVE
    //this.worker.ProgressChanged += this.ProgressChanged;
}


public ICommand InstigateWorkCommand
{
    get { return this.instigateWorkCommand; }
}

private int _currentProgress;
public int CurrentProgress
{
    get { return this._currentProgress; }
    private set
    {
        if (this._currentProgress != value)
        {
            this._currentProgress = value;
            OnPropertyChanged("CurrentProgress");
        }
    }
}

//REMOVE
//private void ProgressChanged(object sender, ProgressChangedEventArgs e)
//{
//    this.CurrentProgress = e.ProgressPercentage;
//}

private void DoWork(object sender, DoWorkEventArgs e)
{
    //ADD NEXT LINES
    isBusy = true;
    Application.Current.Dispatcher.BeginInvoke(
            (Action)instigateWorkCommand.RaiseCanExecuteChanged);

    // do time-consuming work here, calling ReportProgress as and when you can   
    for (int i = 0; i <= 100; i++)
    {
        Thread.Sleep(10);
        _currentProgress = i;
        OnPropertyChanged("CurrentProgress");
    }

    //ADD NEXT LINES
    isBusy = false;  
    Application.Current.Dispatcher.BeginInvoke(
        (Action)instigateWorkCommand.RaiseCanExecuteChanged);
}

bool m_fileSelected = true;  //CHANGE TO SEE THE EFFECT

//REMOVE
//RelayCommand m_loadWordDocCmd;
///// <summary>
///// Relaycommand for Function loadWordDocument
///// </summary>
//public RelayCommand LoadWordDocCmd
//{
//    get
//    {
//        if (this.m_loadWordDocCmd == null)
//        {
//            this.m_loadWordDocCmd = new RelayCommand(this.loadWordDocument, canLoadWordDoc);
//        }
//        return m_loadWordDocCmd;
//    }
//    private set
//    {
//        this.m_loadWordDocCmd = value;
//    }
//}

/// <summary>
/// checks if the Word Document can be loaded
/// </summary>
/// <param name="parameter">not used</param>
/// <returns>if it could Execute, then true, else false</returns>
private bool canLoadWordDoc(object parameter)
{
    bool ret = false;

    if (this.m_fileSelected)
    {
        ret = true;
    }
    return ret;
}

/// <summary>
/// Function for Load Word Document
/// </summary>
/// <param name="parameter">not used</param>
private void loadWordDocument(object parameter)
{
}

Hope this helps.

Upvotes: 0

dymanoid
dymanoid

Reputation: 15197

I hope I understand your issue correctly.

The basic rule for a GUI application is: don't use the GUI thread for (time-consuming) data processing. You have to perform this task on a background thread.

Since you're using .NET 4.0 Client Profile, the async/await feature is not available to you. That would be the easiest solution, however.

You can do this with a ThreadPool instead. The BackgroundWorker is not recommended anymore.

In your XAML, you're binding the ProgressBar.Value property to a AddNewProgressBarValue property, so I assume you have a view-model with that property already. You have to ensure that changing AddNewProgressBarValue will raise the PropertyChanged event. And the good news is, the WPF Binding Engine automatically marshals the property value transfer operation to the GUI thread, so you don't need to care about which thread is changing a property your progress bar is bound to.

So the solution might look like this (not a production code, just an idea!):

class ViewModel : INotifyPropertyChanged
{
    private bool isProcessing;
    public bool AddNewProgressBarVisible
    {
        get { return this.isProcessing; }
        // SetProperty here is a PRISM-like helper to set the backing field value
        // and to raise the PropertyChanged event when needed.
        // You might be using something similar.
        private set { this.SetProperty(ref this.isProcessing, value, "AddNewProgressBarVisible");
    }

    private int progressValue;
    public int AddNewProgressBarValue
    {
        get { return this.progressValue; }
        private set { this.SetProperty(ref this.progressValue, value, "AddNewProgressBarValue");
    }

    // This is your command handler
    private void LoadWordDocument(object parameter)
    {
        if (this.isProcessing)
        {
            // don't allow multiple operations at the same time
            return;
        }

        // indicate that we're staring an operation:
        // AddNewProgressBarVisible will set isProcessing = true
        this.AddNewProgressBarVisible = true;
        this.AddNewProgressBarValue = 0;

        // Notify the bound button, that it has to re-evaluate its state.
        // Effectively, this disables the button.
        this.LoadWordDocCmd.RaiseCanExecuteChanged();

        // Run the processing on a background thread.
        ThreadPool.QueueUserWorkItem(this.DoLoadWordDocument);
    }

    private void DoLoadWordDocument(object state)
    {
        // Do your document loading here,
        // this method will run on a background thread.
        // ...

        // You can update the progress bar value directly:
        this.AddNewProgressBarValue = 42; // ...estimate the value first

        // When you're done, don't forget to enable the button.
        this.AddNewProgressBarVisible = false;

        // We have to marshal this to the GUI thread since your ICommand
        // implementation doesn't do this automatically
        Application.Current.Dispatcher.Invoke(() => this.LoadWordDocCmd.RaiseCanExecuteChanged());
    }

    // this is your command enabler method
    private bool CanLoadWordDoc(object parameter)
    {
        // if we're already loading a document, the command should be disabled
        return this.m_fileSelected && !this.isProcessing;
    }
}

Upvotes: 1

Related Questions