MicrosoftEdge / WebView2Feedback

Feedback and discussions about Microsoft Edge WebView2
https://aka.ms/webview2
451 stars 55 forks source link

[Problem/Bug]: Failed to execute 'showSaveFilePicker' on 'Window #4690

Open leyulin opened 3 months ago

leyulin commented 3 months ago

What happened?

Description

I am current have a blazor project. I was invoking window.showSaveFilePicker from interops. However I have to wait a stream fully processed then show this dialog and I have no clue how long the stream will be finished. (depends on the uploaded file size or if multiple files are selected will be a promise chain) Right now a SecurityError dom exception throw after 1-2 sec. It was asking for user activation.

<Line>Failed to execute 'showSaveFilePicker' on 'Window': Must be handling a user gesture to show a file picker.</Line>
<Line>Error: Failed to execute 'showSaveFilePicker' on 'Window': Must be handling a user gesture to show a file picker.</Line>

Investigation

I checked showSaveFilePicker does require Transient user activation. However in my case I have to wait the stream. I can't find a way to overcome this issue.

Question

Is there any possible ways I can bypass that restriction or manually trigger the activation?

Importance

Blocking. My app's basic functions are not working due to this issue.

Runtime Channel

Stable release (WebView2 Runtime)

Runtime Version

No response

SDK Version

No response

Framework

Other

Operating System

Windows 11

OS Version

No response

Repro steps

See Above

Repros in Edge Browser

Yes, issue can be reproduced in the corresponding Edge version

Regression

No, this never worked

Last working version (if regression)

No response

4Ever20 commented 3 months ago

I have the same issue :-(

Master-Ukulele commented 3 months ago

You mentioned about like large/multiple files cause the stream processed longer, and a SecurityError blocks. Can you explain the steps to repro this on WebView2 sample App or Edge browser? Or provide an example to repro? We will have better understanding on the case, thanks.

leyulin commented 3 months ago

Sure. @Master-Ukulele let me know if this is enough.

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Row Selection and Save Dialog</title>
<style>
    .grid {
        display: grid;
        grid-template-columns: repeat(1, 3fr);
        gap: 5px;
    }

    .row {
        padding: 10px;
        border: 1px solid #ccc;
        cursor: pointer;
    }

    .row.selected {
        background-color: lightblue;
    }

    .context-menu {
        display: none;
        position: absolute;
        background-color: #fff;
        border: 1px solid #ccc;
        padding: 5px;
    }
</style>
</head>
<body>
<div class="grid">
    <div class="row">Row 1</div>
    <div class="row">Row 2</div>
    <div class="row">Row 3</div>
</div>

<div class="context-menu" id="contextMenu">
    <button id="saveBtn">Save</button>
</div>

<script>
    const rows = document.querySelectorAll('.row');
    const contextMenu = document.getElementById('contextMenu');
    const saveBtn = document.getElementById('saveBtn');

    let selectedRows = [];

    rows.forEach(row => {
        row.addEventListener('click', () => {
            row.classList.toggle('selected');
            if (row.classList.contains('selected')) {
                selectedRows.push(row);
            } else {
                selectedRows = selectedRows.filter(selectedRow => selectedRow !== row);
            }
        });

        row.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            contextMenu.style.display = 'block';
            contextMenu.style.left = e.pageX + 'px';
            contextMenu.style.top = e.pageY + 'px';
            saveBtn.onclick = () => {
                selectedRows.forEach(selectedRow => {
                    // Perform save operation here for each selected row
                    window.showSaveFilePicker().then(() => {
                        // File saved successfully
                        console.log(`Row ${selectedRow.textContent} saved.`);
                    }).catch(error => {
                        console.error(error);
                    });
                });

                contextMenu.style.display = 'none';
            }
        });
    });

    window.addEventListener('click', () => {
        contextMenu.style.display = 'none';
    });
</script>
</body>
</html>

For example I have above html image

Master-Ukulele commented 3 months ago

Tried with your example html. I didn't see repro, probably hard to get into the error manually. I also have two questions based on your description.

  1. In the step 3, do you mean if you wait for a few seconds, the next operation will be fine, but if you act immediately the error will show? Does this match the like if it is a large file, you have to wait for something like stream be finished to do the next operation, manually or programmatically?
  2. You mentioned you want 3 dialog pop up accordingly. I think the showSaveFilePicker is a modal dialog, it will prevent you do so, if you force to do it programmatically, it probably causes that error. It sounds like you programmatically hack to race with the streams to open the modal dialog, but as it's a feature by design, it will prevent you at later point, so you get the requirement of transient user activation. That's my guess, please ignore it if wrong.

Would you mind taking a look at SaveAsUIShowing event, instead of using showSaveFilePicker, this may be helpful for your case.

leyulin commented 3 months ago

For 1. No. I mean if I wait for a few seconds then click the cancel button the error will still throw. So I have to do it very fast with all these dialogs. For 2. Due to some limitations :/ end up with this approach. Thank you @Master-Ukulele I will check that event. If not I will make a example with further details.

leyulin commented 3 months ago

Hi @Master-Ukulele SaveAsUIShowing not what I need. Here is the html example I want to clarify regrading above conversion

I used https://github.com/MicrosoftEdge/WebView2Samples/tree/main/SampleApps/WebView2SampleWinComp to dmeo this

<!DOCTYPE html>
<html>
  <head>
    <title>Transient Activation Demo</title>
  </head>
  <body>
    <p>Click the button once and wait - the save file picker will fail to open as it requires transient activation. The error message can be seen in the console.</p>
    <p>Note: clicking additional times or doing any other actions while waiting may cause the transient activation to be reactivated and this demo will not produce the expected error</p>
    <button>Click to show file picker</button>
    <script>
        document.querySelector("button").addEventListener("click", async e => {
            // Wait 5 seconds for the transient activation to expire
            // In our application we are calling some code on the server that will take some arbitrary time to run
            // When these server actions take a long time to run, the call to showSaveFilePicker will fail as the transient activation has expired
            await new Promise(r => setTimeout(r, 5000));
            await window.showSaveFilePicker()
        });
    </script>
  </body>
</html>

Snipaste_2024-08-02_15-19-39

I want ask if there is workaround to overcome similar problem thank you!

EdwardLiuWTG commented 2 months ago

I have the same issue :-(

Master-Ukulele commented 2 months ago

Hi all, thanks for providing the repro steps and the demo. I'm clearer how the transient problem happens. However, as you may know it's the feature and requirement of the showSaveFilePicker. The transient activation typically lasts for a very short period, usually a few hundred milliseconds. WebView2 or Edge can't change this.

@leyulin from your scenario, if you want to achieve the goal in your App. You might have to 1) know or control when the upload stream finishes; 2) use the showSaveFilePicker immediately to keep the transient activate. Here's a possible solution draft

<!DOCTYPE html>
<html>
  <head>
    <title>Transient Activation Possible Workaround</title>
  </head>
  <body>
    <p>Upload the file and wait the stream be finished, then call showSaveFilePicker</p>
    <button id="uploadButton">Click to upload</button>
    <button id="saveButton" disabled>Click to show file picker</button>
    <script>
      document.getElementById("uploadButton").addEventListener("click", async e => {
        // Perform the promise based on when the stream will be finished.
        await new Promise(r => setTimeout(r, 5000));

        // Enable the button.
        const saveButton = document.getElementById("saveButton");
        saveButton.disabled = false;
        saveButton.addEventListener("click", async () => {
          await window.showSaveFilePicker();
        });
      });
    </script>
  </body>
</html>