{"id":112672,"date":"2026-09-03T07:00:00","date_gmt":"2026-09-03T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=112672"},"modified":"2026-09-03T22:05:04","modified_gmt":"2026-09-04T05:05:04","slug":"20260903-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260903-00\/?p=112672","title":{"rendered":"The case of the progress callback that never got called when progress happened"},"content":{"rendered":"<p>A colleague was trying to figure out why their progress handler wasn&#8217;t being called.<\/p>\n<pre>\/\/ C#\r\n\r\nasync Task&lt;bool&gt; DownloadItemAsync(string id)\r\n{\r\n    var op = item.DownloadAsync(id);\r\n    op.Progress += (s, pct) UpdateProgress(pct);\r\n    var result = await op;\r\n    ClearProgress();\r\n    return result;\r\n}\r\n<\/pre>\n<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>\n<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>\n<p>I suggested that they step through the <code>Download\u00adAsync<\/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>\n<p>My colleague came back with the answer. Here&#8217;s the code for <code>Download\u00adAsync<\/code>:<\/p>\n<pre>\/\/ C++\/WinRT\r\n\r\nwinrt::IAsyncOperationWithProgress&lt;bool, double&gt;\r\n    AggregateSource::DownloadAsync(winrt::hstring id)\r\n{\r\n    std::wstring_view idview { id };\r\n    auto pos = idview.find(L':');\r\n    if (pos == std::wstring_view::npos) {\r\n        co_return false;\r\n    }\r\n\r\n    auto providerId = Unescape(idview.substr(0, pos - 1));\r\n\r\n    auto provider = GetProvider(providerId);\r\n    if (!provider) {\r\n        co_return false;\r\n    }\r\n\r\n    auto providerItemId = Unescape(idview.substr(pos + 1));\r\n    co_return co_await provider.DownloadAsync(providerItemId);\r\n}\r\n<\/pre>\n<p>The <code>Aggregate\u00adSource<\/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>\n<p>We look up the provider, and then ask the provider to download the item.<\/p>\n<p>Do you see the problem?<\/p>\n<p>The <code>Download\u00adAsync<\/code> does not generate any progress reports!<\/p>\n<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>\n<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\u00adOperation\u00adWith\u00adProgress<\/code> that it has access to is the one returned by the <code>Aggregate\u00adSource::<wbr \/>Download\u00adAsync<\/code> method.<\/p>\n<p>The easy solution here is to get rid of the middle man and just return the provider&#8217;s <code>IAsync\u00adOperation\u00adWith\u00adProgress<\/code>. That way, the caller can connect to the underlying operation&#8217;s progress.<\/p>\n<pre>winrt::IAsyncOperationWithProgress&lt;bool, double&gt;\r\n    AggregateSource::DownloadAsync(winrt::hstring id)\r\n{\r\n    std::wstring_view idview { id };\r\n    auto pos = idview.find(L':');\r\n    if (pos == std::wstring_view::npos) {\r\n        <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>\r\n    }\r\n\r\n    auto providerId = Unescape(idview.substr(0, pos - 1));\r\n\r\n    auto provider = GetProvider(providerId);\r\n    if (!provider) {\r\n        <span style=\"border: solid 1px currentcolor;\">return completed_async(false);<\/span>\r\n    }\r\n\r\n    auto providerItemId = Unescape(idview.substr(pos + 1));\r\n    <span style=\"border: solid 1px currentcolor;\">return provider.DownloadAsync(providerItemId);<\/span>\r\n}\r\n<\/pre>\n<p>If you don&#8217;t believe in <code>completed_<wbr \/>async<\/code>, you can just write<\/p>\n<pre>        return [] -&gt; winrt::IAsyncOperationWithProgress&lt;bool, double&gt; {\r\n            return false;\r\n        }();\r\n<\/pre>\n<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>\n","protected":false},"excerpt":{"rendered":"<p>Are you talking to the right guy?<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-112672","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Are you talking to the right guy?<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112672","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=112672"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112672\/revisions"}],"predecessor-version":[{"id":112673,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112672\/revisions\/112673"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=112672"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=112672"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=112672"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}