diff --git a/SampleApps/WebView2APISample/AppWindow.cpp b/SampleApps/WebView2APISample/AppWindow.cpp index f692d5ce..aae3d6b5 100644 --- a/SampleApps/WebView2APISample/AppWindow.cpp +++ b/SampleApps/WebView2APISample/AppWindow.cpp @@ -32,6 +32,7 @@ #include "ScenarioAuthentication.h" #include "ScenarioClientCertificateRequested.h" #include "ScenarioCookieManagement.h" +#include "ScenarioCustomDataPartition.h" #include "ScenarioCustomDownloadExperience.h" #include "ScenarioCustomScheme.h" #include "ScenarioCustomSchemeNavigate.h" @@ -537,6 +538,11 @@ bool AppWindow::ExecuteWebViewCommands(WPARAM wParam, LPARAM lParam) NewComponent(this); return true; } + case IDM_SCENARIO_CUSTOM_DATA_PARTITION: + { + NewComponent(this); + return true; + } case IDM_SCENARIO_ADD_HOST_OBJECT: { NewComponent(this); @@ -2314,7 +2320,6 @@ void AppWindow::RegisterEventHandlers() { if (!m_shouldHandleNewWindowRequest) { - args->put_Handled(FALSE); return S_OK; } wil::com_ptr args_as_comptr = args; diff --git a/SampleApps/WebView2APISample/ScenarioCustomDataPartition.cpp b/SampleApps/WebView2APISample/ScenarioCustomDataPartition.cpp new file mode 100644 index 00000000..3bb7ee0f --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioCustomDataPartition.cpp @@ -0,0 +1,502 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "ScenarioCustomDataPartition.h" + +#include "AppWindow.h" +#include "CheckFailure.h" + +using namespace Microsoft::WRL; + +static constexpr WCHAR c_samplePath[] = L"ScenarioCustomDataPartition.html"; +static constexpr int c_minNewWindowSize = 100; +static constexpr WCHAR c_childWindowClassName[] = L"ScenarioCustomDataPartitionChildHostWindow"; + +static RECT GetPopupWindowRectFromUri(const std::wstring& uri) +{ + int left = 100; + int top = 100; + int width = 800; + int height = 600; + + if (uri.find(L"ScenarioCustomDataPartitionChild.html") != std::wstring::npos) + { + width = 600; + height = 800; + } + + if (width < c_minNewWindowSize) + { + width = c_minNewWindowSize; + } + if (height < c_minNewWindowSize) + { + height = c_minNewWindowSize; + } + + RECT rect = {left, top, left + width, top + height}; + return rect; +} + +bool ScenarioCustomDataPartition::EnsureChildWindowClassRegistered() +{ + static bool s_registered = false; + if (s_registered) + { + return true; + } + + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(WNDCLASSEXW); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = ScenarioCustomDataPartition::ChildWndProcStatic; + windowClass.hInstance = GetModuleHandle(nullptr); + windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW); + windowClass.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + windowClass.lpszClassName = c_childWindowClassName; + + if (!RegisterClassExW(&windowClass)) + { + DWORD error = GetLastError(); + if (error != ERROR_CLASS_ALREADY_EXISTS) + { + return false; + } + } + + s_registered = true; + return true; +} + +LRESULT CALLBACK ScenarioCustomDataPartition::ChildWndProcStatic( + HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_NCCREATE) + { + auto* createStruct = reinterpret_cast(lParam); + auto* self = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(self)); + } + + auto* self = reinterpret_cast(GetWindowLongPtr(hWnd, GWLP_USERDATA)); + if (self) + { + return self->ChildWndProc(hWnd, message, wParam, lParam); + } + return DefWindowProc(hWnd, message, wParam, lParam); +} + +LRESULT ScenarioCustomDataPartition::ChildWndProc( + HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_CLOSE: + CloseChildWindow(); + return 0; + case WM_SIZE: + ResizeChildWebView(); + break; + case WM_DESTROY: + if (hWnd == m_childWindow) + { + m_childWindow = nullptr; + m_childController = nullptr; + m_childWebView = nullptr; + } + break; + } + + return DefWindowProc(hWnd, message, wParam, lParam); +} + +void ScenarioCustomDataPartition::ResizeChildWebView() +{ + if (!m_childWindow || !m_childController) + { + return; + } + + RECT bounds = {}; + if (GetClientRect(m_childWindow, &bounds)) + { + CHECK_FAILURE(m_childController->put_Bounds(bounds)); + } +} + +void ScenarioCustomDataPartition::CloseChildWindow() +{ + if (m_childWebView && m_childWebMessageReceivedToken.value != 0) + { + m_childWebView->remove_WebMessageReceived(m_childWebMessageReceivedToken); + m_childWebMessageReceivedToken = {}; + } + + if (m_childController) + { + m_childController->Close(); + m_childController = nullptr; + } + m_childWebView = nullptr; + + if (m_childWindow) + { + HWND childWindow = m_childWindow; + m_childWindow = nullptr; + DestroyWindow(childWindow); + } +} + +ScenarioCustomDataPartition::ScenarioCustomDataPartition(AppWindow* appWindow) + : m_appWindow(appWindow), m_webView(appWindow->GetWebView()) +{ + m_sampleUri = m_appWindow->GetLocalUri(c_samplePath); + + // Disable host-level popup handling so this scenario can fully own NewWindowRequested. + m_appWindow->EnableHandlingNewWindowRequest(false); + + // Register event handlers + RegisterEventHandlers(); + + // Navigate to the sample page + CHECK_FAILURE(m_webView->Navigate(m_sampleUri.c_str())); +} + +void ScenarioCustomDataPartition::RegisterEventHandlers() +{ + // Turn off this scenario if we navigate away from the sample page + CHECK_FAILURE(m_webView->add_ContentLoading( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string uri; + sender->get_Source(&uri); + if (uri.get() != m_sampleUri) + { + m_appWindow->DeleteComponent(this); + } + return S_OK; + }) + .Get(), + &m_contentLoadingToken)); + + // Handle messages from the HTML UI + CHECK_FAILURE(m_webView->add_WebMessageReceived( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string message; + CHECK_FAILURE(args->TryGetWebMessageAsString(&message)); + HandleWebMessage(message.get()); + return S_OK; + }) + .Get(), + &m_webMessageReceivedToken)); + + //! [CustomDataPartitionNewWindowRequested] + // Register a handler for the NewWindowRequested event. + // This handler captures child windows by creating them and setting partitions. + CHECK_FAILURE(m_webView->add_NewWindowRequested( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT + { + // Get a deferral - we'll complete this asynchronously + wil::com_ptr deferral; + CHECK_FAILURE(args->GetDeferral(&deferral)); + + wil::com_ptr argsAsComPtr = args; + + if (m_childWindow || m_childController) + { + CloseChildWindow(); + } + + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Uri(&uri)); + std::wstring targetUri = uri.get() ? uri.get() : L""; + + RECT popupRect = GetPopupWindowRectFromUri(targetUri); + + if (!EnsureChildWindowClassRegistered()) + { + SendStatusToUI(L"Error: Failed to register child window class."); + CHECK_FAILURE(argsAsComPtr->put_Handled(FALSE)); + CHECK_FAILURE(deferral->Complete()); + return S_OK; + } + + m_childWindow = CreateWindowExW( + WS_EX_CONTROLPARENT, c_childWindowClassName, + L"Custom Data Partition Child Window", + WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, popupRect.left, + popupRect.top, popupRect.right - popupRect.left, + popupRect.bottom - popupRect.top, nullptr, nullptr, + GetModuleHandle(nullptr), this); + + if (!m_childWindow) + { + SendStatusToUI(L"Error: Failed to create child native window."); + CHECK_FAILURE(argsAsComPtr->put_Handled(FALSE)); + CHECK_FAILURE(deferral->Complete()); + return S_OK; + } + + ShowWindow(m_childWindow, SW_SHOWNORMAL); + UpdateWindow(m_childWindow); + + auto environment = m_appWindow->GetWebViewEnvironment(); + if (!environment) + { + SendStatusToUI(L"Error: WebView2 environment not available for child window."); + CloseChildWindow(); + CHECK_FAILURE(argsAsComPtr->put_Handled(FALSE)); + CHECK_FAILURE(deferral->Complete()); + return S_OK; + } + + CHECK_FAILURE(environment->CreateCoreWebView2Controller( + m_childWindow, + Callback( + [this, argsAsComPtr, deferral, targetUri]( + HRESULT result, ICoreWebView2Controller* controller) -> HRESULT + { + if (result == S_OK && controller) + { + m_childController = controller; + CHECK_FAILURE(m_childController->get_CoreWebView2(&m_childWebView)); + SetPartitionOnLastChild(L"DefaultPartitionForChild"); + CHECK_FAILURE(m_childWebView->add_WebMessageReceived( + Callback( + [this]( + ICoreWebView2* sender, + ICoreWebView2WebMessageReceivedEventArgs* args) + -> HRESULT + { + wil::unique_cotaskmem_string message; + CHECK_FAILURE(args->TryGetWebMessageAsString(&message)); + + if (std::wstring(message.get()) == L"getCustomPartition") + { + wil::com_ptr + webViewExperimental; + webViewExperimental = + m_childWebView.try_query< + ICoreWebView2Experimental20>(); + + if (!webViewExperimental) + { + SendStatusToChildUI( + L"Error: Custom partition API unavailable."); + return S_OK; + } + + wil::unique_cotaskmem_string partitionId; + HRESULT partitionHr = + webViewExperimental + ->get_CustomDataPartitionId(&partitionId); + + if (SUCCEEDED(partitionHr)) + { + if (partitionId.get() == nullptr || + wcslen(partitionId.get()) == 0) + { + SendStatusToChildUI( + L"Child partition: (default/none)"); + } + else + { + std::wstring status = + L"Child partition: '" + + std::wstring(partitionId.get()) + + L"'"; + SendStatusToChildUI(status); + } + } + else + { + SendStatusToChildUI( + L"Error: Failed to read child partition."); + } + } + + return S_OK; + }) + .Get(), + &m_childWebMessageReceivedToken)); + + auto childWebView3 = m_childWebView.try_query(); + if (childWebView3) + { + CHECK_FAILURE(childWebView3->SetVirtualHostNameToFolderMapping( + L"appassets.example", L"assets", + COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS)); + } + + ResizeChildWebView(); + CHECK_FAILURE(argsAsComPtr->put_NewWindow(m_childWebView.get())); + CHECK_FAILURE(argsAsComPtr->put_Handled(TRUE)); + if (!targetUri.empty()) + { + CHECK_FAILURE(m_childWebView->Navigate(targetUri.c_str())); + } + SendStatusToUI( + L"Child window created and captured! Ready to set partition."); + } + else + { + CloseChildWindow(); + SendStatusToUI( + L"Error: Failed to create child WebView controller."); + CHECK_FAILURE(argsAsComPtr->put_Handled(FALSE)); + } + + CHECK_FAILURE(deferral->Complete()); + return S_OK; + }) + .Get())); + + return S_OK; + }) + .Get(), + &m_newWindowRequestedToken)); + //! [CustomDataPartitionNewWindowRequested] +} + +void ScenarioCustomDataPartition::HandleWebMessage(const std::wstring& message) +{ + // Parse simple command messages from JavaScript + if (message.find(L"setPartition:") == 0) + { + // Extract partition ID from message + std::wstring partitionId = message.substr(13); // Skip "setPartition:" + SetPartitionOnLastChild(partitionId); + } + else if (message == L"checkPartition") + { + CheckCurrentPartition(); + } +} + +void ScenarioCustomDataPartition::SetPartitionOnLastChild(const std::wstring& partitionId) +{ + if (!m_childWebView) + { + SendStatusToUI(L"Error: No child window available. Create a child window first."); + return; + } + + //! [CustomDataPartitionSet] + // Try to set custom data partition using the experimental API + wil::com_ptr webViewExperimental; + webViewExperimental = m_childWebView.try_query(); + + if (!webViewExperimental) + { + SendStatusToUI(L"Error: Custom data partition API (ICoreWebView2Experimental20) not available in this WebView2 version."); + return; + } + + // Set the partition ID on the child window + HRESULT hr = webViewExperimental->put_CustomDataPartitionId(partitionId.c_str()); + //! [CustomDataPartitionSet] + + if (SUCCEEDED(hr)) + { + std::wstring status = L"✓ Success! Partition '" + partitionId + + L"' set on child window."; + SendStatusToUI(status); + } + else + { + SendStatusToUI(L"Error: Failed to set custom data partition."); + } +} + +void ScenarioCustomDataPartition::CheckCurrentPartition() +{ + //! [CustomDataPartitionGet] + // Check the partition of the parent window (should be empty/default) + wil::com_ptr webViewExperimental; + webViewExperimental = m_webView.try_query(); + + if (!webViewExperimental) + { + SendStatusToUI(L"Error: Custom data partition API not available."); + return; + } + + wil::unique_cotaskmem_string partitionId; + HRESULT hr = webViewExperimental->get_CustomDataPartitionId(&partitionId); + //! [CustomDataPartitionGet] + + if (SUCCEEDED(hr)) + { + if (partitionId.get() == nullptr || wcslen(partitionId.get()) == 0) + { + SendStatusToUI(L"✓ Parent window partition: (default/none) - Correct! Parent has no custom partition."); + } + else + { + std::wstring status = L"⚠ Parent window partition: '" + std::wstring(partitionId.get()) + + L"' (Note: Parent should typically remain in default partition)"; + SendStatusToUI(status); + } + } + else + { + SendStatusToUI(L"Error: Failed to get partition ID."); + } +} + +void ScenarioCustomDataPartition::SendStatusToUI(const std::wstring& status) +{ + // Send status message back to the HTML UI + // Escape single quotes in the status message + std::wstring escapedStatus = status; + size_t pos = 0; + while ((pos = escapedStatus.find(L"'", pos)) != std::wstring::npos) + { + escapedStatus.replace(pos, 1, L"\\'"); + pos += 2; + } + + std::wstring script = L"updateStatus('" + escapedStatus + L"');"; + CHECK_FAILURE(m_webView->ExecuteScript(script.c_str(), nullptr)); +} + +void ScenarioCustomDataPartition::SendStatusToChildUI(const std::wstring& status) +{ + if (!m_childWebView) + { + return; + } + + std::wstring escapedStatus = status; + size_t pos = 0; + while ((pos = escapedStatus.find(L"'", pos)) != std::wstring::npos) + { + escapedStatus.replace(pos, 1, L"\\'"); + pos += 2; + } + + std::wstring script = L"updateChildPartitionStatus('" + escapedStatus + L"');"; + CHECK_FAILURE(m_childWebView->ExecuteScript(script.c_str(), nullptr)); +} + +ScenarioCustomDataPartition::~ScenarioCustomDataPartition() +{ + // Restore default host-level popup handling. + if (m_appWindow) + { + m_appWindow->EnableHandlingNewWindowRequest(true); + } + + CloseChildWindow(); + + m_webView->remove_ContentLoading(m_contentLoadingToken); + m_webView->remove_WebMessageReceived(m_webMessageReceivedToken); + m_webView->remove_NewWindowRequested(m_newWindowRequestedToken); +} diff --git a/SampleApps/WebView2APISample/ScenarioCustomDataPartition.h b/SampleApps/WebView2APISample/ScenarioCustomDataPartition.h new file mode 100644 index 00000000..00cf23ca --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioCustomDataPartition.h @@ -0,0 +1,60 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once +#include "stdafx.h" + +#include + +#include "AppWindow.h" +#include "ComponentBase.h" + +// This component demonstrates custom data partition management for child windows. +// It shows how to: +// 1. Create child windows and capture them via NewWindowRequested event +// 2. Apply custom data partitions to child windows only (not parent) +// 3. Create blob URLs from images in the parent window +// 4. Test blob URL accessibility across partition boundaries +// +// Key pattern: In NewWindowRequested handler: +// 1. Get deferral +// 2. Create a native window for the popup +// 3. Create a WebView controller for that window +// 4. In controller-created callback, provide args->put_NewWindow(childWebView) +// 5. Complete deferral +class ScenarioCustomDataPartition : public ComponentBase +{ +public: + ScenarioCustomDataPartition(AppWindow* appWindow); + ~ScenarioCustomDataPartition() override; + static LRESULT CALLBACK ChildWndProcStatic( + HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + +private: + AppWindow* m_appWindow = nullptr; + wil::com_ptr m_webView; + std::wstring m_sampleUri; + EventRegistrationToken m_contentLoadingToken = {}; + EventRegistrationToken m_webMessageReceivedToken = {}; + EventRegistrationToken m_newWindowRequestedToken = {}; + EventRegistrationToken m_childWebMessageReceivedToken = {}; + + // Track a single child popup window for partition management + HWND m_childWindow = nullptr; + wil::com_ptr m_childController; + wil::com_ptr m_childWebView; + + // Event handlers + LRESULT ChildWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + static bool EnsureChildWindowClassRegistered(); + void CloseChildWindow(); + void ResizeChildWebView(); + + void RegisterEventHandlers(); + void HandleWebMessage(const std::wstring& message); + void SetPartitionOnLastChild(const std::wstring& partitionId); + void CheckCurrentPartition(); + void SendStatusToUI(const std::wstring& status); + void SendStatusToChildUI(const std::wstring& status); +}; diff --git a/SampleApps/WebView2APISample/WebView2APISample.rc b/SampleApps/WebView2APISample/WebView2APISample.rc index 5904d534..4ce69982 100644 --- a/SampleApps/WebView2APISample/WebView2APISample.rc +++ b/SampleApps/WebView2APISample/WebView2APISample.rc @@ -274,6 +274,7 @@ BEGIN MENUITEM "Shared worker WebResourceRequested", IDM_SCENARIO_SHARED_WORKER END MENUITEM "WebRTC UDP Port Configuration", IDM_SCENARIO_WEBRTC_UDP_PORT_CONFIGURATION + MENUITEM "Custom Data Partition for Child Windows", IDM_SCENARIO_CUSTOM_DATA_PARTITION POPUP "Custom Download Experience" BEGIN MENUITEM "Use Deferred Download Dialog", IDM_SCENARIO_USE_DEFERRED_DOWNLOAD diff --git a/SampleApps/WebView2APISample/WebView2APISample.vcxproj b/SampleApps/WebView2APISample/WebView2APISample.vcxproj index ec3c5d66..a4dfe8ab 100644 --- a/SampleApps/WebView2APISample/WebView2APISample.vcxproj +++ b/SampleApps/WebView2APISample/WebView2APISample.vcxproj @@ -231,6 +231,7 @@ + @@ -295,6 +296,7 @@ + @@ -380,6 +382,15 @@ $(OutDir)\assets + + $(OutDir)\assets + + + $(OutDir)\assets + + + $(OutDir)\assets + $(OutDir)\assets diff --git a/SampleApps/WebView2APISample/assets/ScenarioCustomDataPartition.html b/SampleApps/WebView2APISample/assets/ScenarioCustomDataPartition.html new file mode 100644 index 00000000..4a324d0a --- /dev/null +++ b/SampleApps/WebView2APISample/assets/ScenarioCustomDataPartition.html @@ -0,0 +1,251 @@ + + + + + + Custom Data Partition Sample + + + +

Custom Data Partition Sample

+ +
+ About this sample: This demonstrates how to create child windows and apply custom data partitions + to isolate storage. It also tests blob URL sharing across partition boundaries. +
+ + +
+

1. Create Child Window

+

Click the button to open the child page. The parent window captures it via the NewWindowRequested event.

+ +
+ + +
+

2. Set Custom Data Partition

+

Enter a partition ID and apply it to the most recently created child window. The parent window will never have a custom partition.

+ + + + +
+ Note: Only child windows should have custom partitions. The parent window remains in the default partition. + Click "Check Parent Window Partition" to verify the parent window has no custom partition set. +
+
+ + +
+

3. Blob URL Creation & Testing

+

This image is displayed in the parent window from assets. A blob URL will be created from it.

+ Sir C V Raman + + + + + + +
+ Test: Create a blob URL from the image, then try to access it from a partitioned child window + to observe cross-partition behavior. +
+
+ + +
+

Status

+
Ready. Click "Create Child Window" to begin.
+
+ + + + diff --git a/SampleApps/WebView2APISample/assets/ScenarioCustomDataPartitionChild.html b/SampleApps/WebView2APISample/assets/ScenarioCustomDataPartitionChild.html new file mode 100644 index 00000000..363de464 --- /dev/null +++ b/SampleApps/WebView2APISample/assets/ScenarioCustomDataPartitionChild.html @@ -0,0 +1,208 @@ + + + + + + Custom Data Partition Child Window + + + +

Custom Data Partition Child Window

+ +
+

This is the dedicated child window page opened by window.open() from the parent scenario page.

+

The parent app captures this window in NewWindowRequested, then you can apply a custom partition from the parent UI.

+
+ +
+ Keep this child window open while setting partition IDs in the parent window. +
+ +
+

Blob URL Image Test

+

Paste a blob URL from the parent window and try loading it here.

+ +
+ + + +
+
Waiting for blob URL input.
+ Blob URL image preview +
+ +
+

Custom Partition

+

Query the child window's current custom data partition from native code.

+ + +
Waiting to query partition.
+
+ +
+

Local Storage Test

+

Use key currentTime to verify storage behavior across partitions.

+ + +
Waiting to test local storage.
+
+ + + + diff --git a/SampleApps/WebView2APISample/assets/Sir_CV_Raman.JPG b/SampleApps/WebView2APISample/assets/Sir_CV_Raman.JPG new file mode 100644 index 00000000..5c302e8c Binary files /dev/null and b/SampleApps/WebView2APISample/assets/Sir_CV_Raman.JPG differ diff --git a/SampleApps/WebView2APISample/resource.h b/SampleApps/WebView2APISample/resource.h index d2474cdc..efdeffbd 100644 --- a/SampleApps/WebView2APISample/resource.h +++ b/SampleApps/WebView2APISample/resource.h @@ -221,6 +221,7 @@ #define IDM_SCENARIO_DEDICATED_WORKER_POST_MESSAGE 3009 #define IDM_SCENARIO_SERVICE_WORKER_POST_MESSAGE 3010 #define IDM_SCENARIO_WEBRTC_UDP_PORT_CONFIGURATION 3011 +#define IDM_SCENARIO_CUSTOM_DATA_PARTITION 3012 #define ID_BLOCKEDSITES 32773 #define ID_SETTINGS_NAVIGATETOSTRING 32774 #define ID_ADD_INITIALIZE_SCRIPT 32775 diff --git a/SampleApps/WebView2Samples.sln b/SampleApps/WebView2Samples.sln index 9ff80692..942f38e2 100644 --- a/SampleApps/WebView2Samples.sln +++ b/SampleApps/WebView2Samples.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30011.22 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36915.13 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebView2APISample", "WebView2APISample\WebView2APISample.vcxproj", "{4F0CEEF3-12B3-425E-9BB0-105200411592}" EndProject