|
| 1 | +Programmatic Save As API |
| 2 | +=== |
| 3 | + |
| 4 | +# Background |
| 5 | + |
| 6 | +Chromium browser's context menus have a "Save as" menu item to save the document |
| 7 | +(html page, image, pdf, or other content) through a save as dialog. We provide |
| 8 | +more flexible ways to programmatically perform the Save As operation in WebView2. |
| 9 | + |
| 10 | +With the new API you will be able to: |
| 11 | +- Launch the default save as dialog |
| 12 | +- Block the default save as dialog |
| 13 | +- Request save as silently by providing the path and save as kind |
| 14 | +- Build your own save as UI |
| 15 | + |
| 16 | +The chromium browser's Save As operation consists of showing the Save As dialog |
| 17 | +and then starting a download of the document. The Save As method and event |
| 18 | +described in this document relate to the Save As dialog and not the download, |
| 19 | +which will go through the existing WebView2 download APIs. |
| 20 | + |
| 21 | +We'd appreciate your feedback. |
| 22 | + |
| 23 | +# Description |
| 24 | + |
| 25 | +We propose the `CoreWebView2.ShowSaveAsUI` method, which allows you to trigger |
| 26 | +the Save As UX programmatically. By using this method, the system default dialog, |
| 27 | +or your own UI will show and start the Save As operation. |
| 28 | + |
| 29 | +We also propose the `CoreWebView2.SaveAsUIShowing` event. You can register this event to block |
| 30 | +the default dialog and instead create your own Save As UI using the `SaveAsUIShowingEventArgs`, |
| 31 | +to set your preferred save as path, save as kind, and duplicate file replacement rule. |
| 32 | +In your client app, you can design your own UI to input these parameters. |
| 33 | +For HTML documents, we support 3 save as kinds: HTML_ONLY, SINGLE_FILE and |
| 34 | +COMPLETE. Non-HTML documents, you must use DEFAULT, which will save the content as |
| 35 | +it is. This API has default values for all parameters, to perform the common |
| 36 | +save as operation. |
| 37 | + |
| 38 | +# Examples |
| 39 | +## Win32 C++ |
| 40 | +### Programmatic Save As |
| 41 | +This example hides the default save as dialog and shows a customized dialog. |
| 42 | +The sample code will register a handler and trigger programmaic save as once. |
| 43 | +```c++ |
| 44 | +bool ScenarioSaveAs::ProgrammaticSaveAs() |
| 45 | +{ |
| 46 | + if (!m_webView2_24) |
| 47 | + return false; |
| 48 | + |
| 49 | + // Register a handler for the `SaveAsUIShowing` event. |
| 50 | + m_webView2_24->add_SaveAsUIShowing( |
| 51 | + Callback<ICoreWebView2SaveAsUIShowingEventHandler>( |
| 52 | + [this]( |
| 53 | + ICoreWebView2* sender, |
| 54 | + ICoreWebView2SaveAsUIShowingEventArgs* args) -> HRESULT |
| 55 | + { |
| 56 | + // Hide the system default save as dialog. |
| 57 | + CHECK_FAILURE(args->put_SuppressDefaultDialog(TRUE)); |
| 58 | + |
| 59 | + auto showCustomizedDialog = [this, args = wil:: make_com_ptr(args)] |
| 60 | + { |
| 61 | + // As an end developer, you can design your own dialog UI, or no UI at all. |
| 62 | + // You can ask the user to provide information like file name, file extension, |
| 63 | + // and so on. Finally, and set them on the event args. |
| 64 | + // |
| 65 | + // This is a customized dialog example, the constructor returns after the |
| 66 | + // dialog interaction is completed by the end user. |
| 67 | + SaveAsDialog dialog; |
| 68 | + if (dialog.confirmed) |
| 69 | + { |
| 70 | + // Setting the SaveAsFilePath, Kind, AllowReplace for the event |
| 71 | + // args from this customized dialog inputs is optional. The event |
| 72 | + // args has default values based on the document to save. |
| 73 | + // |
| 74 | + // Additionally, you can use `get_ContentMimeType` to check the mime |
| 75 | + // type of the document that will be saved to help the Kind selection. |
| 76 | + CHECK_FAILURE( |
| 77 | + args->put_SaveAsFilePath((LPCWSTR)dialog.path.c_str())); |
| 78 | + CHECK_FAILURE(args->put_Kind(dialog.selectedKind)); |
| 79 | + CHECK_FAILURE(args->put_AllowReplace(dialog.allowReplace)); |
| 80 | + } |
| 81 | + else |
| 82 | + { |
| 83 | + // Save As cancelled from this customized dialog. |
| 84 | + CHECK_FAILURE(args->put_Cancel(TRUE)); |
| 85 | + } |
| 86 | + }; |
| 87 | + |
| 88 | + wil::com_ptr<ICoreWebView2Deferral> deferral; |
| 89 | + CHECK_FAILURE(args->GetDeferral(&deferral)); |
| 90 | + |
| 91 | + m_appWindow->RunAsync( |
| 92 | + [deferral, showCustomizedDialog]() |
| 93 | + { |
| 94 | + showCustomizedDialog(); |
| 95 | + CHECK_FAILURE(deferral->Complete()); |
| 96 | + }); |
| 97 | + return S_OK; |
| 98 | + }) |
| 99 | + .Get(), |
| 100 | + &m_SaveAsUIShowingToken); |
| 101 | + |
| 102 | + // Call method ShowSaveAsUI to trigger the programmatic save as once. |
| 103 | + m_webView2_24->ShowSaveAsUI( |
| 104 | + Callback<ICoreWebView2ShowSaveAsUICompletedHandler>( |
| 105 | + [this](HRESULT errorCode, COREWEBVIEW2_SAVE_AS_UI_RESULT result) -> HRESULT |
| 106 | + { |
| 107 | + // Show ShowSaveAsUI returned result, optional. See |
| 108 | + // COREWEBVIEW2_SAVE_AS_UI_RESULT for more details. |
| 109 | + MessageBox( |
| 110 | + m_appWindow->GetMainWindow(), |
| 111 | + (L"Save As " + saveAsUIString[result]).c_str(), L"Info", MB_OK); |
| 112 | + return S_OK; |
| 113 | + }) |
| 114 | + .Get()); |
| 115 | + return true; |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +## .Net/ WinRT |
| 120 | +### Programmatic Save As |
| 121 | +This example hides the default save as dialog and shows a customized dialog. |
| 122 | +The sample code will register a handler and trigger programmaic save as once. |
| 123 | +```c# |
| 124 | + |
| 125 | +async void ProgrammaticSaveAsExecuted(object target, ExecutedRoutedEventArgs e) |
| 126 | +{ |
| 127 | + // Register a handler for the `SaveAsUIShowing` event. |
| 128 | + webView.CoreWebView2.SaveAsUIShowing += (sender, args) => |
| 129 | + { |
| 130 | + // Hide the system default save as dialog. |
| 131 | + args.SuppressDefaultDialog = true; |
| 132 | + |
| 133 | + // Developer can obtain a deferral for the event so that the CoreWebView2 |
| 134 | + // doesn't examine the properties we set on the event args until |
| 135 | + // after the deferral completes asynchronously. |
| 136 | + CoreWebView2Deferral deferral = args.GetDeferral(); |
| 137 | + |
| 138 | + // We avoid potential reentrancy from running a message loop in the event |
| 139 | + // handler. Show the customized dialog later then complete the deferral |
| 140 | + // asynchronously. |
| 141 | + System.Threading.SynchronizationContext.Current.Post((_) => |
| 142 | + { |
| 143 | + using (deferral) |
| 144 | + { |
| 145 | + // This is a customized dialog example. |
| 146 | + var dialog = new SaveAsDialog(); |
| 147 | + if (dialog.ShowDialog() == true) |
| 148 | + { |
| 149 | + // Setting parameters of event args from this dialog is optional. |
| 150 | + // The event args has default values. |
| 151 | + // |
| 152 | + // Additionally, you can use `args.ContentMimeType` to check the mime |
| 153 | + // type of the document that will be saved to help the Kind selection. |
| 154 | + args.SaveAsFilePath = System.IO.Path.Combine( |
| 155 | + dialog.Directory.Text, dialog.Filename.Text); |
| 156 | + args.Kind = (CoreWebView2SaveAsKind)dialog.Kind.SelectedItem; |
| 157 | + args.AllowReplace = (bool)dialog.AllowReplaceOldFile.IsChecked; |
| 158 | + } |
| 159 | + else |
| 160 | + { |
| 161 | + // Save As cancelled from this customized dialog. |
| 162 | + args.Cancel = true; |
| 163 | + } |
| 164 | + } |
| 165 | + }, null); |
| 166 | + }; |
| 167 | + |
| 168 | + // Call ShowSaveAsUIAsync method to trigger the programmatic save as once. |
| 169 | + CoreWebView2SaveAsUIResult result = await webView.CoreWebView2.ShowSaveAsUIAsync(); |
| 170 | + // Show ShowSaveAsUIAsync returned result, optional. See |
| 171 | + // CoreWebView2SaveAsUIResult for more details. |
| 172 | + MessageBox.Show(result.ToString(), "Info"); |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +# API Details |
| 177 | +## Win32 C++ |
| 178 | +```c++ |
| 179 | +/// Specifies save as kind selection options for |
| 180 | +/// `ICoreWebView2SaveAsUIShowingEventArgs`. |
| 181 | +/// |
| 182 | +/// For HTML documents, we support 3 save as kinds: HTML_ONLY, SINGLE_FILE and |
| 183 | +/// COMPLETE. Non-HTML documents, you must use DEFAULT. MIME type of `text/html`, |
| 184 | +/// `application/xhtml+xml` are considered as HTML documents. |
| 185 | +[v1_enum] typedef enum COREWEBVIEW2_SAVE_AS_KIND { |
| 186 | + /// Default to save for a non-html content. If it is selected for a html |
| 187 | + /// page, it's same as HTML_ONLY option. |
| 188 | + COREWEBVIEW2_SAVE_AS_KIND_DEFAULT, |
| 189 | + /// Save the page as html. It only saves top-level document, excludes |
| 190 | + /// subresource. |
| 191 | + COREWEBVIEW2_SAVE_AS_KIND_HTML_ONLY, |
| 192 | + /// Save the page as mhtml. |
| 193 | + /// Read more about mhtml at (https://en.wikipedia.org/wiki/MHTML) |
| 194 | + COREWEBVIEW2_SAVE_AS_KIND_SINGLE_FILE, |
| 195 | + /// Save the page as html, plus, download the page related source files |
| 196 | + /// (for example CSS, JavaScript, images, and so on) in a directory with |
| 197 | + /// the same filename prefix. |
| 198 | + COREWEBVIEW2_SAVE_AS_KIND_COMPLETE, |
| 199 | +} COREWEBVIEW2_SAVE_AS_KIND; |
| 200 | + |
| 201 | +/// Status of a programmatic save as call, indicates the result |
| 202 | +/// for method `ShowSaveAsUI`. |
| 203 | +[v1_enum] typedef enum COREWEBVIEW2_SAVE_AS_UI_RESULT { |
| 204 | + /// The ShowSaveAsUI method call completed successfully. By defaut the the system |
| 205 | + /// save as dialog will open. If `SuppressDefaultDialog` is set to TRUE, will skip |
| 206 | + /// the system dialog, and start the download. |
| 207 | + COREWEBVIEW2_SAVE_AS_UI_RESULT_SUCCESS, |
| 208 | + /// Could not perform Save As because the destination file path is an invalid path. |
| 209 | + /// |
| 210 | + /// It is considered as invalid when the path is empty, a relative path, a directory, |
| 211 | + /// or the parent path doesn't exist. |
| 212 | + COREWEBVIEW2_SAVE_AS_UI_RESULT_INVALID_PATH, |
| 213 | + /// Could not perform Save As because the destination file path already exists and |
| 214 | + /// replacing files was not allowed by the `AllowReplace` property. |
| 215 | + COREWEBVIEW2_SAVE_AS_UI_RESULT_FILE_ALREADY_EXISTS, |
| 216 | + /// Could not perform Save As when the `Kind` property selection not |
| 217 | + /// supported because of the content MIME type or system limits. |
| 218 | + /// |
| 219 | + /// MIME type limits please see the emun `COREWEBVIEW2_SAVE_AS_KIND`. |
| 220 | + /// |
| 221 | + /// System limits might happen when select `HTML_ONLY` for an error page at child |
| 222 | + /// mode, select `COMPLETE` and WebView running in an App Container, etc. |
| 223 | + COREWEBVIEW2_SAVE_AS_UI_RESULT_KIND_NOT_SUPPORTED, |
| 224 | + /// Did not perform Save As because the end user cancelled or the |
| 225 | + /// CoreWebView2SaveAsUIShowingEventArgs.Cancel property was set to TRUE. |
| 226 | + COREWEBVIEW2_SAVE_AS_UI_RESULT_CANCELLED, |
| 227 | +} COREWEBVIEW2_SAVE_AS_UI_RESULT; |
| 228 | + |
| 229 | +[uuid(15e1c6a3-c72a-4df3-91d7-d097fbec3bfd), object, pointer_default(unique)] |
| 230 | +interface ICoreWebView2_24 : IUnknown { |
| 231 | + /// Programmatically trigger a save as action for the currently loaded document. |
| 232 | + /// The `SaveAsUIShowing` event will be raised. |
| 233 | + /// |
| 234 | + /// Opens a system modal dialog by default. If the `SuppressDefaultDialog` is TRUE, |
| 235 | + /// won't open the system dialog. |
| 236 | + /// |
| 237 | + /// The method can return a detailed info to indicate the call's result. |
| 238 | + /// Please see COREWEBVIEW2_SAVE_AS_UI_RESULT. |
| 239 | + /// |
| 240 | + /// \snippet ScenarioSaveAs.cpp ProgrammaticSaveAs |
| 241 | + HRESULT ShowSaveAsUI([in] ICoreWebView2ShowSaveAsUICompletedHandler* handler); |
| 242 | + |
| 243 | + /// Add an event handler for the `SaveAsUIShowing` event. This event is raised |
| 244 | + /// when save as is triggered, programmatically or manually. |
| 245 | + HRESULT add_SaveAsUIShowing( |
| 246 | + [in] ICoreWebView2SaveAsUIShowingEventHandler* eventHanlder, |
| 247 | + [out] EventRegistrationToken* token); |
| 248 | + |
| 249 | + /// Remove an event handler previously added with `add_SaveAsUIShowing`. |
| 250 | + HRESULT remove_SaveAsUIShowing( |
| 251 | + [in] EventRegistrationToken token); |
| 252 | +} |
| 253 | + |
| 254 | +/// The event handler for the `SaveAsUIShowing` event. |
| 255 | +[uuid(55b86cd2-adfd-47f1-9cef-cdfb8c414ed3), object, pointer_default(unique)] |
| 256 | +interface ICoreWebView2SaveAsUIShowingEventHandler : IUnknown { |
| 257 | + HRESULT Invoke( |
| 258 | + [in] ICoreWebView2* sender, |
| 259 | + [in] ICoreWebView2SaveAsUIShowingEventArgs* args); |
| 260 | +} |
| 261 | + |
| 262 | +/// The event args for `SaveAsUIShowing` event. |
| 263 | +[uuid(80101027-b8c3-49a1-a052-9ea4bd63ba47), object, pointer_default(unique)] |
| 264 | +interface ICoreWebView2SaveAsUIShowingEventArgs : IUnknown { |
| 265 | + /// Get the Mime type of content to be saved. |
| 266 | + [propget] HRESULT ContentMimeType([out, retval] LPWSTR* value); |
| 267 | + |
| 268 | + /// You can set this to TRUE to cancel the Save As. Then the download won't start. |
| 269 | + /// A programmatic call will return COREWEBVIEW2_SAVE_AS_UI_RESULT_CANCELLED as well. |
| 270 | + /// |
| 271 | + /// The default value is FALSE. |
| 272 | + /// |
| 273 | + /// Set the `Cancel` for save as. |
| 274 | + [propput] HRESULT Cancel ([in] BOOL value); |
| 275 | + |
| 276 | + /// Get the `Cancel` for save as. |
| 277 | + [propget] HRESULT Cancel ([out, retval] BOOL* value); |
| 278 | + |
| 279 | + /// Indicates if the system default dialog will be suppressed, FALSE means |
| 280 | + /// save as default dialog will show; TRUE means a silent save as, will |
| 281 | + /// skip the system dialog. |
| 282 | + /// |
| 283 | + /// The default value is FALSE. |
| 284 | + /// |
| 285 | + /// Set the `SuppressDefaultDialog`. |
| 286 | + [propput] HRESULT SuppressDefaultDialog([in] BOOL value); |
| 287 | + |
| 288 | + /// Get the `SuppressDefaultDialog`. |
| 289 | + [propget] HRESULT SuppressDefaultDialog([out, retval] BOOL* value); |
| 290 | + |
| 291 | + /// Returns an `ICoreWebView2Deferral` object. This will defer showing the |
| 292 | + /// default Save As dialog and performing the Save As operation. |
| 293 | + HRESULT GetDeferral([out, retval] ICoreWebView2Deferral** deferral); |
| 294 | + |
| 295 | + /// `SaveAsFilePath` is absolute full path of the location. It includes the file name |
| 296 | + /// and extension. If `SaveAsFilePath` is not valid, for example the root drive does |
| 297 | + /// not exist, save as will be denied and return COREWEBVIEW2_SAVE_AS_UI_RESULT_INVALID_PATH. |
| 298 | + /// |
| 299 | + /// If the associated download completes successfully, a target file will be saved at |
| 300 | + /// this location. If the Kind property is `COREWEBVIEW2_SAVE_AS_KIND_COMPLETE`, |
| 301 | + /// there will be an additional directory with resources files. |
| 302 | + /// |
| 303 | + /// The default value is a system suggested path, based on users' local environment. |
| 304 | + /// |
| 305 | + /// Set the `SaveAsFilePath` for save as. |
| 306 | + [propput] HRESULT SaveAsFilePath ([in] LPCWSTR value); |
| 307 | + |
| 308 | + /// Get the `SaveAsFilePath` for save as. |
| 309 | + [propget] HRESULT SaveAsFilePath ([out, retval] LPWSTR* value); |
| 310 | + |
| 311 | + /// `AllowReplace` allows you to control what happens when a file already |
| 312 | + /// exists in the file path to which the Save As operation is saving. |
| 313 | + /// Setting this TRUE allows existing files to be replaced. |
| 314 | + /// Setting this FALSE will not replace existing files and will return |
| 315 | + /// COREWEBVIEW2_SAVE_AS_UI_RESULT_FILE_ALREADY_EXISTS. |
| 316 | + /// |
| 317 | + /// The default value is FALSE. |
| 318 | + /// |
| 319 | + /// Set if allowed to replace the old file if duplicate happens in the save as. |
| 320 | + [propput] HRESULT AllowReplace ([in] BOOL value); |
| 321 | + |
| 322 | + /// Get the duplicates replace rule for save as. |
| 323 | + [propget] HRESULT AllowReplace ([out, retval] BOOL* value); |
| 324 | + |
| 325 | + /// How to save documents with different kind. See the enum |
| 326 | + /// COREWEBVIEW2_SAVE_AS_KIND for a description of the different options. |
| 327 | + /// If the kind isn't allowed for the current document, |
| 328 | + /// COREWEBVIEW2_SAVE_AS_UI_RESULT_KIND_NOT_SUPPORTED will be returned from |
| 329 | + /// ShowSaveAsUI. |
| 330 | + /// |
| 331 | + /// The default value is COREWEBVIEW2_SAVE_AS_KIND_DEFAULT. |
| 332 | + /// |
| 333 | + /// Set the kind for save as. |
| 334 | + [propput] HRESULT Kind ([in] COREWEBVIEW2_SAVE_AS_KIND value); |
| 335 | + |
| 336 | + /// Get the kind for save as. |
| 337 | + [propget] HRESULT Kind ([out, retval] COREWEBVIEW2_SAVE_AS_KIND* value); |
| 338 | +} |
| 339 | + |
| 340 | +/// Receive the result for `ShowSaveAsUI` method. |
| 341 | +[uuid(1a02e9d9-14d3-41c6-9581-8d6e1e6f50fe), object, pointer_default(unique)] |
| 342 | +interface ICoreWebView2ShowSaveAsUICompletedHandler : IUnknown { |
| 343 | + HRESULT Invoke([in] HRESULT errorCode, [in] COREWEBVIEW2_SAVE_AS_UI_RESULT result); |
| 344 | +} |
| 345 | +``` |
| 346 | + |
| 347 | +## .Net/ WinRT |
| 348 | +```c# (but really MIDL3) |
| 349 | +namespace Microsoft.Web.WebView2.Core |
| 350 | +{ |
| 351 | + |
| 352 | + runtimeclass CoreWebView2SaveAsUIShowingEventArgs; |
| 353 | + runtimeclass CoreWebView2; |
| 354 | + |
| 355 | + enum CoreWebView2SaveAsUIResult |
| 356 | + { |
| 357 | + Success = 0, |
| 358 | + InvalidPath = 1, |
| 359 | + FileAlreadyExists = 2, |
| 360 | + KindNotSupported = 3, |
| 361 | + Cancelled = 4, |
| 362 | + }; |
| 363 | + |
| 364 | + enum CoreWebView2SaveAsKind |
| 365 | + { |
| 366 | + Default = 0, |
| 367 | + HtmlOnly = 1, |
| 368 | + SingleFile = 2, |
| 369 | + Complete = 3, |
| 370 | + }; |
| 371 | + |
| 372 | + runtimeclass CoreWebView2SaveAsUIShowingEventArgs |
| 373 | + { |
| 374 | + String ContentMimeType { get; }; |
| 375 | + Boolean Cancel { get; set; }; |
| 376 | + Boolean SuppressDefaultDialog { get; set; }; |
| 377 | + String SaveAsFilePath { get; set; }; |
| 378 | + Boolean AllowReplace { get; set; }; |
| 379 | + CoreWebView2SaveAsKind Kind { get; set; }; |
| 380 | + Windows.Foundation.Deferral GetDeferral(); |
| 381 | + }; |
| 382 | + |
| 383 | + runtimeclass CoreWebView2 |
| 384 | + { |
| 385 | + // ... |
| 386 | + |
| 387 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2_24")] |
| 388 | + { |
| 389 | + event Windows.Foundation.TypedEventHandler |
| 390 | + <CoreWebView2, CoreWebView2SaveAsUIShowingEventArgs> SaveAsUIShowing; |
| 391 | + Windows.Foundation.IAsyncOperation<CoreWebView2SaveAsUIResult > |
| 392 | + ShowSaveAsUIAsync(); |
| 393 | + } |
| 394 | + }; |
| 395 | +} |
| 396 | +``` |
0 commit comments