Skip to content

Commit 9ccb1c8

Browse files
Merge pull request #3777 from MicrosoftEdge/api-programmaticsaveas
Add spec for ProgrammaticSaveAs.md
2 parents af8423b + 67b4ab1 commit 9ccb1c8

File tree

1 file changed

+396
-0
lines changed

1 file changed

+396
-0
lines changed

specs/ProgrammaticSaveAs.md

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
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

Comments
 (0)