WebView2Browser Sample Application

repository·main·Indexed 19 days ago

https://github.com/microsoftedge/webview2browser

A sample Windows desktop application demonstrating the capabilities of the Microsoft Edge WebView2 control. Built with C++ and JavaScript, it showcases a multi-WebView architecture to separate application UI from web content, implementing features such as tab management, browser navigation (Back, Forward, Reload, Stop), and communication between WebViews using ICoreWebView2 and ICoreWebView2Controller APIs.

Tokens
4.9K
Snippets
8
Records
15
Agent score
17%

What's inside WebView2Browser

  1. Manage Browser History using IndexedDB

    main

    The browser manages history by using standard web technologies within the Controls WebView. When a tab's URI changes, the host application triggers a message that causes the Controls WebView to store the new history item in IndexedDB.

    This approach allows the history UI to be implemented entirely in JavaScript, using queryDB to interact with an objectStore named 'history'. The implementation uses an index (e.g., stampedURI) to check for existing entries for a URI on the current date to prevent duplicates and update timestamps instead.

    // JavaScript: Adding a history item to IndexedDB
    function addHistoryItem(item, callback) {
        queryDB((db) => {
            let transaction = db.transaction(['history'], 'readwrite');
            let historyStore = transaction.objectStore('history');
            // ... logic to check existing index and add/update item ...
        });
    }
  2. Communicate between WebViews using WebView2 APIs

    main

    WebView2Browser uses a message-passing architecture to synchronize the UI (Controls WebView) and the content (Tabs WebViews).

    On the C++ (Host) side:

    • Use PostWebMessageAsJson to send JSON data to a WebView.
    • Use add_WebMessageReceived to listen for messages sent from a WebView.
    • Implement ICoreWebView2WebMessageReceivedEventHandler to handle incoming messages.

    On the JavaScript side:

    • Use window.chrome.webview.postMessage(message) to send data to the host.
    • Use window.chrome.webview.addEventListener('message', handler) to listen for messages from the host.
    // JavaScript: Sending a message to the host
    function reloadActiveTabContent() {
        var message = {
            message: commands.MG_RELOAD,
            args: {}
        };
        window.chrome.webview.postMessage(message);
    }
    
    // JavaScript: Listening for messages from the host
    function init() {
        window.chrome.webview.addEventListener('message', messageHandler);
    }
  3. Understand the multi-WebView architecture

    main

    WebView2Browser uses a multi-WebView approach to separate the application UI from web content. This isolation allows the UI to be built with web technologies (HTML/CSS/JS) while maintaining separate user data directories for security and functionality.

    There are two primary types of WebView environments:

    1. UI WebViews: Used for application controls and options dropdowns. These use a dedicated UI environment.
    2. Content WebViews: One per tab, used to display actual web content. These use a separate content environment.

    This separation enables features like fetching favicons from the web and using IndexedDB for storing favorites and history within the UI context.

  4. Build the WebView2Browser sample

    main

    To build the WebView2Browser application, follow these steps:

    1. Clone the repository.
    2. Open the solution in Visual Studio 2019 (or Visual Studio 2017 by changing the Platform Toolset in Project Properties > Configuration properties > General > Platform Toolset and updating the Windows SDK).
    3. Set the target architecture and configuration (e.g., Debug/Release, x86/x64).
    4. Build the solution. WebView2 is included via NuGet package.

    Note for Windows versions below Windows 10: You must modify DPI handling code to ensure compatibility.

    In WebViewBrowserApp.cpp, replace SetProcessDpiAwarenessContext with SetProcessDPIAware:

    // Call SetProcessDPIAware() instead when using Windows 7 or any version
    // below 1703 (Windows 10).
    SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

    In BrowserWindow.cpp, remove the GetDpiForWindow call in GetDPIAwareBound:

    int BrowserWindow::GetDPIAwareBound(int bound)
    {
        // Remove the GetDpiForWindow call when using Windows 7 or any version
        // below 1607 (Windows 10).
        return (bound * GetDpiForWindow(m_hWnd) / DEFAULT_DPI);
    }

    Ensure you perform a clean build after making these changes.

  5. Implement Back, Forward, Reload, and Stop navigation

    main

    Browser navigation controls (Back, Forward, Reload, Stop) are typically implemented as buttons in a UI WebView. When clicked, these buttons post a web message to the host application. The host application then calls the corresponding methods on the active tab's ICoreWebView2 instance.

    UI CommandHost MethodDescription
    MG_GO_BACKGoBack()Navigates to the previous page in history
    MG_GO_FORWARDGoForward()Navigates to the next page in history
    MG_RELOADReload()Reloads the current page
    MG_CANCELCallDevToolsProtocolMethod(L"Page.stopLoading", ...)Stops the current navigation

    JavaScript (UI side):

    document.querySelector('#btn-back').addEventListener('click', function() {
        window.chrome.webview.postMessage({ message: commands.MG_GO_BACK, args: {} });
    });

    C++ (Host side):

    case MG_GO_BACK:
        m_tabs.at(m_activeTabId)->m_contentWebView->GoBack();
        break;
    case MG_CANCEL:
        m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Page.stopLoading", L"{}", nullptr);
        break;
  6. Set up WebView2 environments for UI and content

    main

    WebView2 allows hosting web content in Windows apps by creating separate environments for the browser's UI and the web content. This isolation ensures that user data for web content (tabs) is kept separate from the browser's own UI data.

    To implement this, use CreateCoreWebView2EnvironmentWithOptions to create a content environment first, then create a separate UI environment using a different user data directory.

        // 1. Create environment for web content (tabs)
        std::wstring userDataDirectory = GetAppDataDirectory();
        userDataDirectory.append(L"\User Data");
    
        HRESULT hr = CreateCoreWebView2EnvironmentWithOptionsWithOptions(nullptr, userDataDirectory.c_str(),
            L"", Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
                [this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
        {
            m_contentEnv = env;
            return InitUIWebViews();
        }).Get());
    
        // 2. Inside InitUIWebViews, create environment for browser UI
        std::wstring browserDataDirectory = GetAppDataDirectory();
        browserDataDirectory.append(L"\Browser Data");
    
        return CreateCoreWebView2EnvironmentWithOptions(nullptr, browserDataDirectory.c_str(),
            L"", Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
                [this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
        {
            m_uiEnv = env;
            // Create UI WebViews (controls, options, etc.)
            return S_OK;
        }).Get());
  7. Navigate to web pages and handle browser schemes

    main

    Navigation can be triggered by entering a URI in the address bar. The host application should intercept web messages from the controls WebView and determine if the URI is a special internal browser page (e.g., browser://favorites, browser://settings, browser://history) or a standard web URL.

    If it is an internal page, navigate the content WebView to the corresponding local HTML file. Otherwise, navigate to the provided URI or use an encoded search URI as a fallback.

    // Handling MG_NAVIGATE message in the host app
    case MG_NAVIGATE:
    {
        std::wstring uri(args.at(L"uri").as_string());
        std::wstring browserScheme(L"browser://");
    
        if (uri.substr(0, browserScheme.size()).compare(browserScheme) == 0)
        {
            // Handle internal browser pages
            std::wstring path = uri.substr(browserScheme.size());
            if (path.compare(L"favorites") == 0 || path.compare(L"settings") == 0 || path.compare(L"history") == 0)
            {
                std::wstring filePath = L"wvbrowser_ui\content_ui\";
                filePath.append(path).append(L".html");
                m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(GetFullPathFor(filePath.c_str()).c_str());
            }
        }
        else
        {
            // Handle standard web navigation
            m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(uri.c_str());
        }
    }
    break;
  8. Update the address bar and navigation state

    main

    To keep the browser UI (address bar, back/forward buttons) in sync with the active tab, register for the add_SourceChanged event on the content WebView.

    When the source changes, the host app should:

    1. Retrieve the new source via get_Source.
    2. Retrieve navigation state via get_CanGoForward and get_CanGoBack.
    3. Post a JSON message (e.g., MG_UPDATE_URI) to the controls WebView to update the UI state.
    // 1. Register event in host app
    RETURN_IF_FAILED(m_contentWebView->add_SourceChanged(Callback<ICoreWebView2SourceChangedEventHandler>(
        [this, browserWindow](ICoreWebView2* webview, ICoreWebView2SourceChangedEventArgs* args) -> HRESULT
    {
        return browserWindow->HandleTabURIUpdate(m_tabId, webview);
    }).Get(), &m_uriUpdateForwarderToken));
    
    // 2. Send update to UI WebView
    HRESULT BrowserWindow::HandleTabURIUpdate(size_t tabId, ICoreWebView2* webview)
    {
        wil::unique_cotaskmem_string source;
        webview->get_Source(&source);
    
        web::json::value jsonObj = web::json::value::parse(L"{}");
        jsonObj[L"message"] = web::json::value(MG_UPDATE_URI);
        jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId);
        jsonObj[L"args"][L"uri"] = web::json::value(source.get());
    
        return PostJsonToWebView(jsonObj, m_controlsWebView.Get());
    }
  9. Listen for Security State Changes

    main

    To update the security icon (e.g., the padlock) in the address bar, you must enable security event listening via the Chrome DevTools Protocol (CDP).

    1. Call CallDevToolsProtocolMethod(L"Security.enable", L"{}", nullptr) on the ICoreWebView2 instance.
    2. Get the ICoreWebView2DevToolsProtocolEventReceiver for the Security.securityStateChanged event.
    3. Register an add_DevToolsProtocolEventReceived handler to catch the event and forward the state to the UI WebView.
    // C++: Enabling security event listening
    RETURN_IF_FAILED(m_contentWebView->CallDevToolsProtocolMethod(L"Security.enable", L"{}", nullptr));
    
    BrowserWindow::CheckFailure(m_contentWebView->GetDevToolsProtocolEventReceiver(L"Security.securityStateChanged", &m_securityStateChangedReceiver), L"");
    
    // Register handler to forward updates
    RETURN_IF_FAILED(m_securityStateChangedReceiver->add_DevToolsProtocolEventReceived(Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
        [this, browserWindow](ICoreWebView2* webview, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
            BrowserWindow::CheckFailure(browserWindow->HandleTabSecurityUpdate(m_tabId, webview, args), "Can't update security icon");
            return S_OK;
        }).Get(), &m_securityUpdateToken));
  10. Implement Tab Creation via Message Passing

    main

    When a user triggers a new tab (e.g., clicking a 'New Tab' button in the UI), the Controls WebView sends a message to the host application. The host then creates a new Tab object and initializes a new WebView for that tab.

    1. UI Trigger: The JavaScript in the Controls WebView calls window.chrome.webview.postMessage with a command like MG_CREATE_TAB.
    2. Host Handling: The C++ host catches the message in its WebMessageReceived handler, parses the tabId and active status, and calls Tab::CreateNewTab.
    3. Tab Initialization: The Tab::Init method creates the CoreWebView2Controller and registers event handlers for HistoryChanged, SourceChanged, NavigationStarting, and NavigationCompleted to keep the UI in sync.
    // JavaScript in Controls WebView
    function createNewTab(shouldBeActive) {
        const tabId = getNewTabId();
        var message = {
            message: commands.MG_CREATE_TAB,
            args: {
                tabId: parseInt(tabId),
                active: shouldBeActive || false
            }
        };
        window.chrome.webview.postMessage(message);
        // ... update local tab state ...
    }