@@ -3,107 +3,105 @@ Programmatic Save As API
33
44# Background
55
6- The context menu has the "Save as" item to manually save the html page, image,
7- pdf, or other content through a save as dialog. We provide more flexiable ways
8- to do the save as programmatically in WebView2. You can bring up the default
9- save as dialog easily. And you will be able to block default dialog, save the
10- content silently, by providing the path and save as kind programmatically or
11- even build your own save as UI.
6+ Chromium browser's context menus have a "Save as" menu item to save the top level
7+ document (html page, image, pdf, or other content) through a save as dialog. We
8+ provide more flexible ways to programmatically perform the Save As operation in
9+ WebView2.
1210
13- In this document we describe the API. We'd appreciate your feedback.
11+ With the new API you will be able to:
12+ - Launch the default save as dialog
13+ - Block the default save as dialog
14+ - Request save as silently by providing the path and save as kind
15+ - Build your own save as UI
16+
17+ The chromium browser's Save As operation consists of showing the Save As dialog
18+ and then starting a download of the top level document. The Save As method and
19+ event described in this document relate to the Save As dialog and not the download,
20+ which will go through the existing WebView2 download APIs.
21+
22+ We'd appreciate your feedback.
1423
1524# Description
1625
1726We propose the ` RequestSaveAs ` method WebView2, which allows you to trigger
18- the save as programmatically. By using this method, the system default dialog,
19- or your own ui will popup .
27+ the Save As UX programmatically. By using this method, the system default dialog,
28+ or your own UI will show and start the Save As operation .
2029
21- We propose the ` SaveAsRequested ` event. You can register this event to block
22- the default dialog and use the ` SaveAsRequestedEventArgs ` instead, to set
23- your preferred save as path, save as kind, and duplicate file replacement rule.
30+ We also propose the ` CoreWebView2. SaveAsRequested` event. You can register this event to block
31+ the default dialog and instead create your own Save As UI using the ` SaveAsRequestedEventArgs ` ,
32+ to set your preferred save as path, save as kind, and duplicate file replacement rule.
2433In your client app, you can design your own UI to input these parameters.
2534For HTML documents, we support 3 save as kinds: HTML_ONLY, SINGLE_FILE and
26- COMPLETE. Non-HTML documents, must use DEFAULT, which will save the content as
35+ COMPLETE. Non-HTML documents, you must use DEFAULT, which will save the content as
2736it is. This API has default values for all parameters, to perform the common
2837save as operation.
2938
3039# Examples
3140## Win32 C++
32- ### Add or Remove the Event Handler
41+ ### Add the Event Handler
3342This example hides the default save as dialog and shows a customized dialog.
3443``` c++
35- bool ScenarioSaveAs::ToggleEventHandler ()
44+ bool ScenarioSaveAs::AddEventHandler ()
3645{
3746 if (!m_webView2_20)
3847 return false;
39- // m_hasSaveAsRequestedEventHandler indicates whether the event handler
40- // has been subscribed.
41- if (m_hasSaveAsRequestedEventHandler)
42- {
43- // Unregister the handler for the `SaveAsRequested` event.
44- m_webView2_20->remove_SaveAsRequested(m_saveAsRequestedToken);
45- }
46- else
47- {
48- // Register a handler for the `SaveAsRequested` event.
49- m_webView2_20->add_SaveAsRequested(
50- Callback<ICoreWebView2SaveAsRequestedEventHandler>(
51- [this](
52- ICoreWebView2* sender,
53- ICoreWebView2SaveAsRequestedEventArgs* args) -> HRESULT
48+
49+ // Register a handler for the `SaveAsRequested` event.
50+ m_webView2_20->add_SaveAsRequested(
51+ Callback<ICoreWebView2SaveAsRequestedEventHandler>(
52+ [this](
53+ ICoreWebView2* sender,
54+ ICoreWebView2SaveAsRequestedEventArgs* args) -> HRESULT
55+ {
56+ // Hide the system default save as dialog.
57+ CHECK_FAILURE (args->put_SuppressDefaultDialog(TRUE));
58+
59+ auto showCustomizedDialog = [ this, args]
5460 {
55- // Hide the system default save as dialog.
56- CHECK_FAILURE (args->put_SuppressDefaultDialog(TRUE));
57-
58- auto showCustomizedDialog = [ this, args]
61+ // Preview the content mime type, optional
62+ wil::unique_cotaskmem_string mimeType;
63+ CHECK_FAILURE(args->get_ContentMimeType(&mimeType));
64+
65+ // As an end developer, you can design your own dialog UI, or no UI at all.
66+ // You can ask the user to provide information like file name, file extension, and so on.
67+ // Finally, and set them on the event args
68+ //
69+ // This is a customized dialog example, the constructor returns after the
70+ // dialog interaction is completed by the end user.
71+ SaveAsDialog dialog(m_appWindow->GetMainWindow(), saveKinds);
72+ if (dialog.confirmed)
73+ {
74+ // Setting the ResultFilePath, Kind, AllowReplace for the event
75+ // args from this customized dialog inputs is optional.
76+ // The event args has default values based on the document to save.
77+ CHECK_FAILURE (
78+ args->put_ResultFilePath((LPCWSTR)dialog.path.c_str()));
79+ CHECK_FAILURE (args->put_Kind(dialog.selectedKind));
80+ CHECK_FAILURE(args->put_AllowReplace(dialog.allowReplace));
81+ }
82+ else
5983 {
60- // Preview the content mime type, optional
61- wil::unique_cotaskmem_string mimeType;
62- CHECK_FAILURE(args->get_ContentMimeType(&mimeType));
63-
64- // As an end developer, you can design your own dialog UI, or no UI at all.
65- // You can ask the user information like file name, file extenstion, and etc.
66- // Finally, concatenate and pass them into the event args
67- //
68- // This is a customized dialog example, the constructor returns after the
69- // dialog interaction is completed by the end user.
70- SaveAsDialog dialog(m_appWindow->GetMainWindow(), saveKinds);
71- if (dialog.confirmed)
72- {
73- // Setting the ResultFilePath, Kind, AllowReplace for the event
74- // args from this customized dialog inputs is optional.
75- // The event args has default values based on the document to save.
76- CHECK_FAILURE (
77- args->put_ResultFilePath((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_saveAsRequestedToken);
101- }
102- m_hasSaveAsRequestedEventHandler = !m_hasSaveAsRequestedEventHandler;
84+ // Save As cancelled from this customized dialog
85+ CHECK_FAILURE(args->put_Cancel(TRUE));
86+ }
87+ };
88+
89+ wil::com_ptr<ICoreWebView2Deferral> deferral;
90+ CHECK_FAILURE (args->GetDeferral(&deferral));
91+
92+ m_appWindow->RunAsync(
93+ [deferral, showCustomizedDialog]()
94+ {
95+ showCustomizedDialog ();
96+ CHECK_FAILURE(deferral->Complete());
97+ });
98+ return S_OK;
99+ })
100+ .Get(),
101+ &m_saveAsRequestedToken);
102+
103103 MessageBox(
104- m_appWindow->GetMainWindow(),
105- (m_hasSaveAsRequestedEventHandler ? L"Event Handler Added" : L"Event Handler Rremoved"), L"Info",
106- MB_OK);
104+ m_appWindow->GetMainWindow(), L"Event Handler Added", L"Info",MB_OK);
107105 return true;
108106}
109107```
@@ -131,18 +129,14 @@ bool ScenarioSaveAs::ProgrammaticSaveAs()
131129```
132130
133131## .Net/ WinRT
134- ### Add or Remove the Event Handler
132+ ### Add the Event Handler
135133This example hides the default save as dialog and shows a customized dialog.
136134``` c#
137135
138- void TaggleEventHandlerExecuted (object target , ExecutedRoutedEventArgs e )
136+ void AddEventHandlerExecuted (object target , ExecutedRoutedEventArgs e )
139137{
140- if (hasSaveAsRequestedEventHandler )
141- webView .CoreWebView2 .SaveAsRequested -= WebView_SaveAsRequested ;
142- else
143- webView .CoreWebView2 .SaveAsRequested += WebView_SaveAsRequested ;
144- hasSaveAsRequestedEventHandler = ! hasSaveAsRequestedEventHandler ;
145- MessageBox .Show (hasSaveAsRequestedEventHandler ? " Event Handler Added" : " Event Handler Rremoved" , " Info" );
138+ webView .CoreWebView2 .SaveAsRequested -= WebView_SaveAsRequested ;
139+ MessageBox .Show (" Event Handler Added" , " Info" );
146140}
147141
148142void WebView_SaveAsRequested (object sender , CoreWebView2SaveAsRequestedEventArgs args )
@@ -167,11 +161,12 @@ void WebView_SaveAsRequested(object sender, CoreWebView2SaveAsRequestedEventArgs
167161 var dialog = new SaveAsDialog (_Kinds );
168162 if (dialog .ShowDialog () == true )
169163 {
170- // Preview the content mime type, optional.
171- args .ContentMimeType ;
172-
173164 // Setting parameters of event args from this dialog is optional.
174165 // The event args has default values.
166+ //
167+ // Additionally, you can use `args.ContentMimeType` to check the mime
168+ // type of the document that will be saved to help setup your custom
169+ // Save As dialog UI
175170 args .ResultFilePath = dialog .Directory .Text + " /" + dialog .Filename .Text ;
176171 args .Kind = (CoreWebView2SaveAsRequestedKind )dialog .Kind .SelectedItem ;
177172 args .AllowReplace = (bool )dialog .AllowReplaceOldFile .IsChecked ;
@@ -199,11 +194,11 @@ async void ProgrammaticSaveAsExecuted(object target, ExecutedRoutedEventArgs e)
199194# API Details
200195## Win32 C++
201196``` c++
202- // / Specifies save as requested kind selection options for `ICoreWebView2_20`,
203- // / used in `SaveAsRequestedEventArgs`
197+ // / Specifies save as requested kind selection options for
198+ // / `ICoreWebView2SaveAsRequestedEventArgs`.
204199// /
205- // / When the source is a html page, supports to select `HTML_ONLY`,
206- // / `SINGLE_FILE`, `COMPLETE`; when the source is a non-html,
200+ // / When the source is an HTML document, `DEFAULT`, `HTML_ONLY`, `SINGLE_FILE`,
201+ // / and `COMPLETE` are valid values. When the source is a non-html,
207202// / only allows to select `DEFAULT`; otherwise, will deny the download
208203// / and return `COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_NOT_SUPPORTED`.
209204// /
@@ -219,8 +214,9 @@ async void ProgrammaticSaveAsExecuted(object target, ExecutedRoutedEventArgs e)
219214 COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_HTML_ONLY,
220215 /// Save the page as mhtml
221216 COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_SINGLE_FILE,
222- /// Save the page as html, plus, download the page related source files in
223- /// a same name directory
217+ /// Save the page as html, plus, download the page related source files
218+ /// (for example CSS, JavaScript, images, and so on) in a directory with
219+ /// the same filename prefix.
224220 COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_COMPLETE,
225221} COREWEBVIEW2_SAVE_AS_REQUESTED_KIND;
226222
@@ -230,8 +226,8 @@ async void ProgrammaticSaveAsExecuted(object target, ExecutedRoutedEventArgs e)
230226 /// Could not perform Save As because the destination file path is an invalid path.
231227 ///
232228 /// It is considered as invalid when:
233- /// the path is empty, a relativate path, the parent directory doesn't
234- /// exist, or the path is a driectory .
229+ /// the path is empty or a relativate path, the parent directory doesn't
230+ /// exist, or the path is a directory .
235231 ///
236232 /// Parent directory can be itself, if the path is root directory, or
237233 /// root disk. When the root doesn't exist, the path is invalid.
@@ -247,16 +243,17 @@ async void ProgrammaticSaveAsExecuted(object target, ExecutedRoutedEventArgs e)
247243 /// System limits might happen when select ` HTML_ONLY ` for an error page,
248244 /// select ` COMPLETE ` and WebView running in an App Container, etc.
249245 COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_NOT_SUPPORTED,
250- /// Did not perform Save As because the client side decided to cancel.
246+ /// Did not perform Save As because the end user cancelled or the
247+ /// CoreWebView2SaveAsRequestedEventArgs.Cancel property was set to TRUE.
251248 COREWEBVIEW2_SAVE_AS_REQUESTED_CANCELLED,
252- /// Save as requested completeed, the downloading would start
249+ /// The RequestSaveAs method call completed successfully and the download has started.
253250 COREWEBVIEW2_SAVE_AS_REQUESTED_COMPLETED,
254251} COREWEBVIEW2_SAVE_AS_REQUESTED_RESULTS;
255252
256253[uuid(15e1c6a3-c72a-4df3-91d7-d097fbec3bfd), object, pointer_default(unique)]
257254interface ICoreWebView2_20 : IUnknown {
258- /// Programmatically trigger a save as action for current content. ` SaveAsRequested `
259- /// event will be raised.
255+ /// Programmatically trigger a save as action for the current top-level document.
256+ /// The ` SaveAsRequested ` event will be raised.
260257 ///
261258 /// Opens a system modal dialog by default. If it was already opened, this method
262259 /// would not open another one. If the ` SuppressDefaultDialog ` is TRUE, won't open
@@ -266,19 +263,17 @@ interface ICoreWebView2_20 : IUnknown {
266263 /// Please see COREWEBVIEW2_SAVE_AS_REQUESTED_RESULTS
267264 ///
268265 /// \snippet ScenarioSaveAs.cpp ProgrammaticSaveAs
269- HRESULT SaveContentAs ([ in] ICoreWebView2RequestSaveAsCompletedHandler* handler);
266+ HRESULT RequestSaveAs ([ in] ICoreWebView2RequestSaveAsCompletedHandler* handler);
270267
271268 /// Add an event handler for the ` SaveAsRequested ` event. This event is raised
272269 /// when save as is triggered, programmatically or manually.
273270 ///
274- /// \snippet ScenarioSaveAs.cpp ToggleEventHandler
271+ /// \snippet ScenarioSaveAs.cpp AddEventHandler
275272 HRESULT add_SaveAsRequested(
276273 [ in] ICoreWebView2SaveAsRequestedEventHandler* eventHanlder,
277274 [ out] EventRegistrationToken* token);
278275
279276 /// Remove an event handler previously added with ` add_SaveAsRequested ` .
280- ///
281- /// \snippet ScenarioSaveAs.cpp ToggleEventHandler
282277 HRESULT remove_SaveAsRequested(
283278 [ in] EventRegistrationToken token);
284279}
@@ -297,9 +292,8 @@ interface ICoreWebView2SaveAsRequestedEventArgs : IUnknown {
297292 /// Get the Mime type of content to be saved
298293 [ propget] HRESULT ContentMimeType([ out, retval] LPWSTR* value);
299294
300- /// Indicates if client side cancelled the silent save as, TRUE means cancelled.
301- /// When the event is invoked, the download won't start. A programmatic call will
302- /// return COREWEBVIEW2_SAVE_AS_CANCELLED as well.
295+ /// You can set this to TRUE to cancel the Save As. Then the download won't start.
296+ /// A programmatic call will return COREWEBVIEW2_SAVE_AS_CANCELLED as well.
303297 ///
304298 /// The default value is FALSE.
305299 ///
@@ -325,14 +319,13 @@ interface ICoreWebView2SaveAsRequestedEventArgs : IUnknown {
325319 /// default Save As dialog and performing the Save As operation.
326320 HRESULT GetDeferral([ out, retval] ICoreWebView2Deferral** deferral);
327321
328- /// ` ResultFilePath ` is absolute full path of the location. It includes the
329- /// file name and extension. If ` ResultFilePath ` is not valid, e.g. root drive
322+ /// ` ResultFilePath ` is absolute full path of the location. It includes the file name
323+ /// and extension. If ` ResultFilePath ` is not valid, for example the root drive does
330324 /// not exist, save as will be denied and return COREWEBVIEW2_SAVE_AS_INVALID_PATH.
331325 ///
332- /// When the download complete and success, a target file will be saved at this
333- /// location. If the select ` COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_COMPLETE ` , will be
334- /// an additional directory with resources files. The directory has the same name
335- /// as filename, at the same location.
326+ /// If the associated download completes successfully, a target file will be saved at
327+ /// this location. If the Kind property is ` COREWEBVIEW2_SAVE_AS_REQUESTED_KIND_COMPLETE ` ,
328+ /// there will be an additional directory with resources files.
336329 ///
337330 /// The default value is a system suggested path, based on users' local environment.
338331 ///
0 commit comments