Click here to Skip to main content
15,891,689 members
Articles / Desktop Programming / WPF

A reusable ProgressViewModel to observe progress with MVVM

Rate me:
Please Sign up or sign in to vote.
4.94/5 (11 votes)
30 Jan 2012CPOL8 min read 40.8K   737   26  
UI and code-behind is executed in different threads. Long running processes need asynchronous execution. This artice shows a way of doing this in a ViewModel approach.
namespace HTMLTitleParser.ViewModel
{
  using System;
  using System.Collections.Generic;
  using System.Windows;
  using System.Windows.Controls;
  using System.Windows.Input;

  using Progress;

  public partial class AppVM : BaseVM
  {
    #region fields
    // Default directory to scan for HTML files
    private string mPath = HTMLTitleParser.View.Main.DirMyDocuments;
    private ProgressVM mProcessing = null;

    private Dictionary<string, string> mMapFile2Title;
    #endregion fields

    #region Constructor
    public AppVM()
    {
      this.Processing = new ProgressVM() { StatusMessage = ProgressVM.StrReady };
      this.mMapFile2Title = new Dictionary<string, string>();
    }
    #endregion Constructor

    #region Enums
    /// <summary>
    /// Types of supported long running processes
    /// </summary>
    internal enum TypeOfProcess
    {
      ProcessHTML = 0,
      MultiProcessHTML = 1
    }
    #endregion Enums

    #region Properties
    /// <summary>
    /// Path to directory containing HTML to be scanned
    /// </summary>
    public string Path
    {
      get
      {
        return this.mPath;
      }

      set
      {
        if (this.mPath != (value == null ? string.Empty : value))
        {
          this.mPath = (value == null ? string.Empty : value);
          this.OnPropertyChanged(Bind.PropName(() => this.Path));
        }
      }
    }

    /// <summary>
    /// Property to display HTML file paths and their HTML TITLE TAG content
    /// </summary>
    public Dictionary<string, string> MapFile2Title
    {
      get
      {
        return (this.mMapFile2Title == null ? new Dictionary<string, string>() : this.mMapFile2Title);
      }
     
      set
      {
        if (this.mMapFile2Title != value)
        {
          this.mMapFile2Title = (value == null ? new Dictionary<string, string>() : value);
          this.OnPropertyChanged(Bind.PropName(() => this.MapFile2Title));
        }
      }
    }

    /// <summary>
    /// Processing ViewModel exposing properties for progressbar
    /// visibility and start/stop GUI enablements.
    /// </summary>  
    public ProgressVM Processing
    {
      get
      {
        return this.mProcessing;
      }

      private set
      {
        this.mProcessing = value;
        this.OnPropertyChanged(Bind.PropName(() => this.Processing));
      }
    }
    #endregion Properties

    #region Methods
    /// <summary>
    /// Determine whether a command can be executed based on the fact
    /// if there is a background process in progress or not.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public void CanExecute_IfNoProcessRuns(object sender, CanExecuteRoutedEventArgs e)
    {
      e.Handled = true;

      if (e != null && this.Processing != null)
      {
        string sProcessName;     // Allow only if there is no other process running
        e.CanExecute = !this.ProcessIsRunning(out sProcessName);
        return;
      }

      e.CanExecute = false;
    }

    /// <summary>
    /// Main entry point of simple HTML processing in the
    /// Applictaion ViewModel. This method does all the housekeeping
    /// to bind GUI controls to their respective properties. Start the
    /// backgound processing and registers another result method
    /// <see cref="AppVM.ProcessHTML_Results"/> to be executed
    /// when the background processing is finished (with or without errors).
    /// </summary>
    /// <param name="e"></param>
    /// <param name="progressBar"></param>
    /// <param name="statusTextBox"></param>
    /// <param name="lstResults"></param>
    /// <param name="btnStop"></param>
    /// <returns></returns>
    public bool ProcessHTMLDirectory(ExecutedRoutedEventArgs e,
                                     ProgressBar progressBar,
                                     TextBox statusTextBox,
                                     ListView statusListView,
                                     ListView lstResults,
                                     Button btnStop)
    {
      e.Handled = true;
      string dirPath = (string)e.Parameter;

      if (this.Processing != null)
      {     // Emergency stop: Don't run twice...
        if (this.Processing.CanCancelRunProcess == true)
          return false;
      }

      try
      {
        // Check if process is currently running or not
        string sProcessName;
        if (this.ProcessIsRunning(out sProcessName) == true)
        {
          MessageBox.Show("A " + sProcessName + " is currently being executed. Only one process can be executed at a time.", "Process is being executed",
                            MessageBoxButton.OK, MessageBoxImage.Information);
          return false;
        }

        this.BindStatusBarItems(TypeOfProcess.ProcessHTML,
                                progressBar, statusTextBox, statusListView,
                                lstResults, btnStop, this.ProcessHTML_Results);

        this.Processing.SetProgressVisibility(true);

        // Force refresh of CanExecute Command framework functions ...
        CommandManager.InvalidateRequerySuggested();

        Dictionary<string, object> callParams = new Dictionary<string, object>();
        callParams.Add(ViewModel.ProcessHTMLVM.KeyDirPath, dirPath);

        // Set parameters (if any) and method to run and run it asynchronously
        this.Processing.RunProcess(true, callParams, ((ProcessHTMLVM)this.Processing).ProcessHTMLDirectory);
      }
      catch (Exception exc)
      {
        MessageBox.Show(exc.ToString());
      }

      return true;
    }

    /// <summary>
    /// Cancel processing of the currently running process
    /// </summary>
    /// <param name="e"></param>
    public void CancelProcessing(ExecutedRoutedEventArgs e)
    {
      e.Handled = true;

      if (this.mProcessing != null)
        this.mProcessing.Cancel();
    }

    /// <summary>
    /// Determine whether a process is currently running or not and return its name (if any)
    /// </summary>
    /// <returns></returns>
    internal bool ProcessIsRunning(out string processName)
    {
      processName = string.Empty;

      if (this.Processing != null)
      {
        if (this.Processing.CanCancelRunProcess == true)
        {
          processName = this.Processing.ProcessName;
          return true;
        }
      }

      return false;
    }

    /// <summary>
    /// This function is called when the <seealso cref="ProcessHTMLDirectory.ProcessHTMLDirectory"/>
    /// completes. The <paramref name="e"/> parameter is used to display the result in the GUI.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void ProcessHTML_Results(object sender, ProcessHTMLVM.ProgressResult e)
    {
      // Force refresh of CanExecute Command framework functions ...
      CommandManager.InvalidateRequerySuggested();

      if (e.Cancel == true || e.Error == true)
      {
        if (e.Cancel == true)
          this.Processing.StatusMessage = string.Format("Cancelled by user with result code {0}", e.ResultCode);
        else
        {
          if (e.InnerException != null)
            this.Processing.StatusMessage = string.Format("{0}, result code:{1}", e.InnerException.Message, e.ResultCode);
          else
            this.Processing.StatusMessage = string.Format("Process finished with errors and result code {0}", e.ResultCode);
        }

        return;
      }

      // Set processing result in application viewmodel and make it visible in GUI
      object o;
      if (e.ResultObjects.TryGetValue(ProcessHTMLVM.KeyMapFileToTitle, out o) == true)
        this.MapFile2Title = o as Dictionary<string, string>;

      // Reset status message to indicate that the system is wating for input
      this.Processing.StatusMessage = ProgressVM.StrReady;
    }

    /// <summary>
    /// Reset GUI bindings of progress elements and construct a new processing class
    /// (eg.: for Backup/Restore operation)
    /// </summary>
    /// <param name="progressBar"></param>
    /// <param name="statusTextBox"></param>
    /// <param name="lstResults"></param>
    /// <param name="btnStop"></param>
    /// <param name="btnExec"></param>
    private void BindStatusBarItems(TypeOfProcess typeOfProcess,
                                    ProgressBar progressBar,
                                    TextBox statusTextBox,
                                    ListView statusListView,
                                    ListView lstResults,
                                    Button btnStop,
                                    EventHandler<ProgressVM.ProgressResult> processCompleted = null)
    {
      ProgressVM retProcess = null;

      switch (typeOfProcess)
      {
        case TypeOfProcess.ProcessHTML:
          retProcess = new ProcessHTMLVM();
          break;

        default:
          throw new NotImplementedException(
                string.Format("The process type: {0} is not supported.", typeOfProcess));
      }

      // Get an initially collapsed progress bar plus binding
      ProgressVM tExec = ProgressVM.BindProgressStatusItems(
                                retProcess, progressBar, statusTextBox, statusListView, lstResults, btnStop);

      tExec.CanRunProcess = false;
      tExec.CanCancelRunProcess = true;

      if (processCompleted != null)
        tExec.resultEvent += processCompleted;

      this.Processing = tExec;
    }
    #endregion Methods
  }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
The Windows Presentation Foundation (WPF) and C# are among my favorites and so I developed Edi

and a few other projects on GitHub. I am normally an algorithms and structure type but WPF has such interesting UI sides that I cannot help myself but get into it.

https://de.linkedin.com/in/dirkbahle

Comments and Discussions