6
\$\begingroup\$

The recommended way to report something as "progress" back from a async function is to use IProgress<Type> or IProgress<Class>.

When the caller invokes the async function, it can provide an implementation for IProgress. This object is then used inside the called instance to report back progress.

For a better description of IProgress, please see Reporting Progress from Async Tasks by Stephen Cleary.

Example code:

Code for the consumer (that wants to receive progress):

//Create progress object
Progress<ProgressDetail> progressRunner = new Progress<ProgressDetail>();
progressRunner.ProgressChanged += RunnerProgressUpdate;

//Pass it to the async function
bool result = await simpleRunner.RunAsync(progressRunner);

Called function (that reports progress back):

public async Task<bool> RunAsync(IProgress<ProgressDetail> Progress)
{
  if (Progress != null)
  {
     ProgressDetail rp = new ProgressDetail();
     rp.Starting = true;
     Progress.Report(rp);
  }
}

Description:

Although the implementation inside the called function is trivial, IMHO it creates too much code for the simple task of reporting back progress. The if check is required because it is perfectly legal that a caller passed in null if no progress is desired.

Beside this, it is also important that the reported object is used only once. Because the object might be handled in an entirely different thread, each call of Report() must yield a new instance.

Quote from Reporting Progress from Async Tasks by Stephen Cleary:

...it means you can’t modify the progress object after it’s passed to Report. It is an error to keep a single “current progress” object, update it, and repeatedly pass it to Report.

Goal:

The goal for this class is:

  • Get rid of the NULL check altogether in order to make the code easier to read (I simply like code that can be read from top to down without conditions whenever possible).

  • Prevent the reuse of the object that is reported as progress and throw an exception if someone tries it anyway (Because this a typical heisenbug that I hate to debug)

  • Allow re-usability of an instance of this class so the usage is more coder-friendly

New class

To solve this, the class ProgressReporter was created. Using this class allows the following code:

ProgressReporter<ProgressDetail> _reporter;

public async Task<bool> RunAsync(IProgress<ProgressDetail> Progress)
{
    //Assign progress reporter
    _reporter = new ProgressReporter<ProgressDetail>(Progress, CreateNewInstanceAfterReport:true);

   _reporter.Content.Starting = true;
   _reporter.Report();

   //Continue to use _reporter...
   ....
}

If you need exact code details, please see

Final note: I know I ended up smurf naming all the things. Suggestions for better naming are very welcome.

Code:

    /// <summary>
    /// A helper class to report status using the IProgress interface and reporting back a class (ReportedObject).
    /// The object beeing reported can used only be ONCE. If CreateNewInstanceAfterReport is FALSE (the default), calling Report again will raise an exception.
    /// If CreateNewInstanceAfterReport is TRUE, a new instance of the reported object will automatically be created after calling Report().
    /// </summary>
    /// <typeparam name="ReportedObject">The object beeing reported when calling Report()</typeparam>
    public class ProgressReporter<ReportedObject> where ReportedObject : class, new()
    {
        IProgress<ReportedObject> _iProgress;

        bool _rearmAfterReport;

        /// <summary>
        /// Provides access to the instance that is beeing reported as the "Progress"
        /// </summary>
        public ReportedObject Content { get; private set; }


        /// <summary>
        /// Create an instance of this class, requires the IProgress<Class> implementation that is used to report progress.
        /// </summary>
        /// <param name="IProgress">IProgress implementation used to report progress</param>
        public ProgressReporter(IProgress<ReportedObject> IProgress)
        {
            _iProgress = IProgress;
            _rearmAfterReport = false;
            Content = new ReportedObject();
        }

        /// <summary>
        /// Create an instance of this class, requires the IProgress<Class> implementation that is used to report progress.
        /// </summary>
        /// <param name="IProgress">IProgress implementation used to report progress</param>
        /// <param name="CreateNewInstanceAfterReport">TRUE if a new instance of Content should automatically be created after Report()</param>
        public ProgressReporter(IProgress<ReportedObject> IProgress, bool CreateNewInstanceAfterReport)
            : this(IProgress)
        {
            _rearmAfterReport = CreateNewInstanceAfterReport;
        }

        /// <summary>
        /// Reports Content back as progress. Can be used only ONCE if RearmAfterReport is FALSE.
        /// </summary>
        public void Report()
        {
            //MTH: We need to make sure that an instance of this class is used only once because ReportedObject might be used in a entire different thread. 
            //Quote from [Reporting Progress from Async Tasks](http://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html) by Stephen Cleary
            //"...it means you can’t modify the progress object after it’s passed to Report. It is an error to keep a single “current progress” object, update it, and repeatedly pass it to Report."
            if (Content != null)
            {
                if (_iProgress != null)
                {
                    _iProgress.Report(Content);
                }

                //Set Content to null no matter if we have an actual IProgress or not to make sure that an incorrect use of this class is detected.
                Content = null;

                //If rearm is set, create a new instance
                if (_rearmAfterReport)
                {
                    Content = new ReportedObject();
                }
            }
            else
            {
                //Seems you didn't read the part about "USE IT ONLY ONCE".
                //Here's an exception for you.
                //You're welcome. 
                throw new InvalidOperationException("An instance of the reported object can only be used once");
            }
        }
\$\endgroup\$
4
\$\begingroup\$
public class ProgressReporter<ReportedObject>

According to Framework Design Guidelines about naming of types, the name of a type parameter should start with a T, here, that would be TReportedObject.

Also, I think that specifying that it's an object is redundant, so something like TReported should be enough.


public ReportedObject Content { get; private set; }

I don't like that the setter is private. Since you usually need to specify multiple properties at the same time, I would prefer to use object initializer:

new ProgressDetail { Starting = true }

But making the setter private means that this is not possible.


public ProgressReporter(IProgress<ReportedObject> IProgress, bool CreateNewInstanceAfterReport)

Again, based on Framework Design Guidelines, this time about naming parameters, parameter names should use camelCase, not PascalCase. Here that would be progress (I don't think iProgress makes sense) and createNewInstanceAfterReport.


_rearmAfterReport = CreateNewInstanceAfterReport;

Why does the field have a different name than the parameter? That's confusing.

Also, if you delegated from the simpler constructor to the more complicated one, it would make your code simpler. Or just have a single constructor overload with an optional parameter instead.

\$\endgroup\$
  • \$\begingroup\$ About camelCasing parameters name: I didn't know this rule exists for parameters but used camelCase for function local variables. Am I right to assume that parameters also should use camelCase because they ARE local variables inside the function? Please explain what you meant by "delegate from the simpler to the more complicated one, it would make your code simpler": Why would this make the code simpler? \$\endgroup\$ – Tex Hex Jan 19 '15 at 8:05
  • \$\begingroup\$ Well, parameters are not the same as local variables, but yeah, I guess they're similar enough to use the same naming convention. \$\endgroup\$ – svick Jan 19 '15 at 13:14
  • \$\begingroup\$ And delegating the other way around would make the code simpler, because you would set _rearmAfterReport only in one place. \$\endgroup\$ – svick Jan 19 '15 at 13:15
  • \$\begingroup\$ Thanks @svick, understood. Will change the code locally here and post a 2.0 question in a few days. \$\endgroup\$ – Tex Hex Jan 19 '15 at 18:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.