The case of the progress callback that never got called when progress happened

The Old New Thing (Raymond Chen) Tools

Summary

The article describes a debugging scenario where a progress callback in a C#/C++/WinRT application wasn't triggered due to an intermediate method not reporting progress, and proposes a solution to bypass the middleman.

<p>A colleague was trying to figure out why their progress handler wasn&#8217;t being called.</p> <pre>// C# async Task&lt;bool&gt; DownloadItemAsync(string id) { var op = item.DownloadAsync(id); op.Progress += (s, pct) UpdateProgress(pct); var result = await op; ClearProgress(); return result; } </pre> <p>This is pretty standard stuff. Start the operation, hook up the progress, and then wait for the operation to complete. But they never got any progress.</p> <p>I asked them to check if maybe the item was downloading so fast that they missed all the progress. But no, even if the download takes a long time, they never get any progress.</p> <p>I suggested that they step through the <code>Download­Async</code> method to see where it raises progress, and then follow the execution to the point where the progress callback is supposed to be invoked, to see why it didn&#8217;t make it. (To be fair, this is a cross-language debugging problem, so it&#8217;s harder than it looks. I suggested just focusing on the C++ side: Wait for the COM-callable wrapper to be generated and set as the progress callback, and then set a breakpoint on that wrapper. If that breakpoint gets hit, but the C# code doesn&#8217;t run, then there is a problem in the projection. If the breakpoint never gets hit, then the problem is on the C++ side.)</p> <p>My colleague came back with the answer. Here&#8217;s the code for <code>Download­Async</code>:</p> <pre>// C++/WinRT winrt::IAsyncOperationWithProgress&lt;bool, double&gt; AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { co_return false; } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { co_return false; } auto providerItemId = Unescape(idview.substr(pos + 1)); co_return co_await provider.DownloadAsync(providerItemId); } </pre> <p>The <code>Aggregate­Source</code> gathers items from multiple providers. The format of the <code>id</code> is a provider, a colon, and then an ID. (The provider ID and item ID are escaped, just in case they themselves happen to contain a colon.)</p> <p>We look up the provider, and then ask the provider to download the item.</p> <p>Do you see the problem?</p> <p>The <code>Download­Async</code> does not generate any progress reports!</p> <p>It never calls <code>co_await winrt::get_progress_token()</code>, much less call the token with a progress value to generate a progress report.</p> <p>It&#8217;s apparent that what the code wants to do when it attaches the progress callback is to receive callbacks from the <i>inner</i> operation, the one that comes from the provider. However, the only <code>IAsync­Operation­With­Progress</code> that it has access to is the one returned by the <code>Aggregate­Source::<wbr />Download­Async</code> method.</p> <p>The easy solution here is to get rid of the middle man and just return the provider&#8217;s <code>IAsync­Operation­With­Progress</code>. That way, the caller can connect to the underlying operation&#8217;s progress.</p> <pre>winrt::IAsyncOperationWithProgress&lt;bool, double&gt; AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { <span style="border: solid 1px currentcolor;">return <a title="Creating an already-completed asynchronous activity in C++/WinRT, part 8" href="https://devblogs.microsoft.com/oldnewthing/20240718-00/?p=109977">completed_async</a>(false);</span> } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { <span style="border: solid 1px currentcolor;">return completed_async(false);</span> } auto providerItemId = Unescape(idview.substr(pos + 1)); <span style="border: solid 1px currentcolor;">return provider.DownloadAsync(providerItemId);</span> } </pre> <p>If you don&#8217;t believe in <code>completed_<wbr />async</code>, you can just write</p> <pre> return [] -&gt; winrt::IAsyncOperationWithProgress&lt;bool, double&gt; { return false; }(); </pre> <p>I said that this is the easy solution. There&#8217;s also a hard solution, which we will have to look at later because I haven&#8217;t written it up yet.</p> <p>The post <a href="https://devblogs.microsoft.com/oldnewthing/20260903-00/?p=112672">The case of the progress callback that never got called when progress happened</a> appeared first on <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>.</p>
Original Article
View Cached Full Text

Cached at: 09/04/26, 11:45 AM

# The case of the progress callback that never got called when progress happened - The Old New Thing Source: [https://devblogs.microsoft.com/oldnewthing/20260903-00?p=112672](https://devblogs.microsoft.com/oldnewthing/20260903-00?p=112672) A colleague was trying to figure out why their progress handler wasn’t being called\. ``` // C# async Task<bool> DownloadItemAsync(string id) { var op = item.DownloadAsync(id); op.Progress += (s, pct) UpdateProgress(pct); var result = await op; ClearProgress(); return result; } ``` This is pretty standard stuff\. Start the operation, hook up the progress, and then wait for the operation to complete\. But they never got any progress\. I asked them to check if maybe the item was downloading so fast that they missed all the progress\. But no, even if the download takes a long time, they never get any progress\. I suggested that they step through the`Download­Async`method to see where it raises progress, and then follow the execution to the point where the progress callback is supposed to be invoked, to see why it didn’t make it\. \(To be fair, this is a cross\-language debugging problem, so it’s harder than it looks\. I suggested just focusing on the C\+\+ side: Wait for the COM\-callable wrapper to be generated and set as the progress callback, and then set a breakpoint on that wrapper\. If that breakpoint gets hit, but the C\# code doesn’t run, then there is a problem in the projection\. If the breakpoint never gets hit, then the problem is on the C\+\+ side\.\) My colleague came back with the answer\. Here’s the code for`Download­Async`: ``` // C++/WinRT winrt::IAsyncOperationWithProgress<bool, double> AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { co_return false; } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { co_return false; } auto providerItemId = Unescape(idview.substr(pos + 1)); co_return co_await provider.DownloadAsync(providerItemId); } ``` The`Aggregate­Source`gathers items from multiple providers\. The format of the`id`is a provider, a colon, and then an ID\. \(The provider ID and item ID are escaped, just in case they themselves happen to contain a colon\.\) We look up the provider, and then ask the provider to download the item\. Do you see the problem? The`Download­Async`does not generate any progress reports\! It never calls`co\_await winrt::get\_progress\_token\(\)`, much less call the token with a progress value to generate a progress report\. It’s apparent that what the code wants to do when it attaches the progress callback is to receive callbacks from the*inner*operation, the one that comes from the provider\. However, the only`IAsync­Operation­With­Progress`that it has access to is the one returned by the`Aggregate­Source::Download­Async`method\. The easy solution here is to get rid of the middle man and just return the provider’s`IAsync­Operation­With­Progress`\. That way, the caller can connect to the underlying operation’s progress\. ``` winrt::IAsyncOperationWithProgress<bool, double> AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { return completed_async(false); } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { return completed_async(false); } auto providerItemId = Unescape(idview.substr(pos + 1)); return provider.DownloadAsync(providerItemId); } ``` If you don’t believe in`completed\_async`, you can just write ``` return [] -> winrt::IAsyncOperationWithProgress<bool, double> { return false; }(); ``` I said that this is the easy solution\. There’s also a hard solution, which we will have to look at later because I haven’t written it up yet\. ### Category ### Topics ## Author ![Raymond Chen](https://devblogs.microsoft.com/oldnewthing/wp-content/uploads/sites/38/2019/02/RaymondChen_5in-150x150.jpg) Raymond has been involved in the evolution of Windows for more than 30 years\. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie\-jeebies\. The Web site spawned a book, coincidentally also titled The Old New Thing \(Addison Wesley 2007\)\. He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information\.

Similar Articles

Understanding the rationale behind a rule when trying to circumvent it

The Old New Thing (Raymond Chen)

This article from Microsoft's Old New Thing blog explains the rationale behind best practices for Windows kernel callback functions, particularly why blocking or waiting on work items defeats their purpose, using a cautionary tale about drivers causing system hangs.

Cancellation of Windows Runtime activities is asynchronous

The Old New Thing (Raymond Chen)

This article explains why cancellation of Windows Runtime asynchronous activities is asynchronous, using code examples to illustrate how it avoids deadlocks, especially when progress callbacks trigger cancellation.

The case of the hang when the user changed keyboard layouts

The Old New Thing (Raymond Chen)

A debugging story about a Windows program hanging when the user changes keyboard layouts due to a background thread that created a window but wasn't pumping messages. The fix is to either pump messages or destroy the window.