Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebView ZoomFactor Resets on each Navigation #3459

Open
RickStrahl opened this issue May 5, 2023 · 14 comments
Open

WebView ZoomFactor Resets on each Navigation #3459

RickStrahl opened this issue May 5, 2023 · 14 comments
Assignees
Labels
bug Something isn't working

Comments

@RickStrahl
Copy link

The ZoomFactor property is getting reset every time the WebView instance is navigated when it should maintain the ZoomFactor across navigations. We are after all looking at the same instance.

Observed:
Any time you create a new WebView instance and change the source or refresh the ZoomFactor is reset to its initial value. If you set the value during Initialization to 2 it resets to 2. Default is 1 and it so it usually resets to 1.

Additionally the ZoomFactorChanged event is fired whenever the page is refreshed and receives the now reset value instead of the current active ZoomFactor the user may have changed to during using the active Web page.

Steps:

  • create a WebView
  • Open a page in the WebView
  • Change the Zoom level with Ctrl-Mousewheel or Ctrl+/-
  • Refresh the page

Observed:

Changing zoom level works, but is not persisted across the refresh operation.

Expected:

Since you are setting the ZoomFactor on the WebBrowser instance I would expect the ZoomFactor to stay at what the user sets it to unless it's explicitly overridden at the code level.

Further the fact that ZoomFactorChanged is fired when the value is reset makes it very difficult to maintain this value explicitly. It would be useful to be able to distinguish between:

  • Initial Assignment
  • Reset on Navigation
  • Changed by user / in document

Without this you need to use flag values to disable level capture in NavigationStarting and enable in DomContentLoaded which is a PITA for something that seems like it should be automatic since you set the value at the WebBrowser instance.

FWIW full browsers behave as follows:

  • Refresh: Zoom level is preserved
  • Load a new page: Zoom level is reset to default (in Edge)
  • Go back to original page: Zoom level is back to now zoomed level (ie. change is remembered)

Workaround

For now the workaround I use requires a bunch of extra code:

        /// <summary>
        /// If true, tracks the Zoom factor of this browser instance
        /// over refreshes. By default the WebView looses any user
        /// zooming via Ctrl+ and Ctrl- or Ctrl-Mousewheel whenever
        /// the content is reloaded. This settings explicitly keeps
        /// track of the ZoomFactor and overrides it.
        /// </summary>
        public bool TrackZoomFactor { get; set; }
        
        public double ZoomLevel { get; set;  }= 1;
        private bool CaptureZoom = false;
        
        /// <summary>
        /// Handle keeping the Zoom Factor the same for all preview instances
        /// Without this 
        /// </summary>
        private void CaptureZoomFactor()
        {
            if (WebBrowser.CoreWebView2 == null)
            {
                throw new InvalidOperationException("Please call CaptureZoomFactor after WebView has been initialized");
            }
            
            if (!TrackZoomFactor)
                return;
            
            WebBrowser.ZoomFactorChanged += (s, e) =>
            {
                if (!CaptureZoom)
                {
                    WebBrowser.ZoomFactor = ZoomLevel;
                }
                else
                {
                    ZoomLevel = WebBrowser.ZoomFactor;
                }
            };
            WebBrowser.NavigationStarting += (object sender, CoreWebView2NavigationStartingEventArgs e) => CaptureZoom = false;
            WebBrowser.CoreWebView2.DOMContentLoaded += (s, e) => CaptureZoom = true;
        }
@RickStrahl RickStrahl added the bug Something isn't working label May 5, 2023
@ajtruckle
Copy link

Double check your code your creating the window because mine was based on the sample and it defaulted the zoom to 1.0 thereby overriding my preference.

@RickStrahl
Copy link
Author

Not sure what you're saying. If you set the value that becomes the default and that's what it reverts to every time. if you initialize with 2 it will always go back to to 2. If you don't set it it always goes back to 1.

@ajtruckle
Copy link

#2451

@ajtruckle
Copy link

Actually, I think the base code that I started with (CWebBrowser) was from another site which has a RegistereEventHandlers function:

void CWebBrowser::RegisterEventHandlers()
{
	// NavigationCompleted handler
	CHECK_FAILURE(m_pImpl->m_webView->add_NavigationCompleted(
		Callback<ICoreWebView2NavigationCompletedEventHandler>(
			[this](
				ICoreWebView2*,
				ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT
			{
				m_isNavigating = false;

				SetZoomFactor(1.0);

				BOOL success;
				CHECK_FAILURE(args->get_IsSuccess(&success));

				if (!success)
				{
					COREWEBVIEW2_WEB_ERROR_STATUS webErrorStatus{};
					CHECK_FAILURE(args->get_WebErrorStatus(&webErrorStatus));
					if (webErrorStatus == COREWEBVIEW2_WEB_ERROR_STATUS_DISCONNECTED)
					{
						// Do something here if you want to handle a specific error case.
						// In most cases this isn't necessary, because the WebView will
						// display its own error page automatically.
					}
				}

				wil::unique_cotaskmem_string uri;
				m_pImpl->m_webView->get_Source(&uri);

				if (wcscmp(uri.get(), L"about:blank") == 0)
				{
					uri = wil::make_cotaskmem_string(L"");
				}

				auto callback = m_callbacks[CallbackType::NavigationCompleted];
				if (callback != nullptr)
					RunAsync(callback);

				return S_OK;
			})
		.Get(),
				&m_navigationCompletedToken));

	// NavigationStarting handler
	CHECK_FAILURE(m_pImpl->m_webView->add_NavigationStarting(
		Callback<ICoreWebView2NavigationStartingEventHandler>(
			[this](
				ICoreWebView2*,
				ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT
			{
				wil::unique_cotaskmem_string uri;
				CHECK_FAILURE(args->get_Uri(&uri));

				m_isNavigating = true;

				return S_OK;
			}).Get(), &m_navigationStartingToken));

	// DocumentTitleChanged handler
	CHECK_FAILURE(m_pImpl->m_webView->add_DocumentTitleChanged(
		Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
			[this](ICoreWebView2* sender, IUnknown* args) -> HRESULT {
				wil::unique_cotaskmem_string title;
				CHECK_FAILURE(sender->get_DocumentTitle(&title));

				m_strTitle = title.get();

				auto callback = m_callbacks[CallbackType::TitleChanged];
				if (callback != nullptr)
					RunAsync(callback);

				return S_OK;
			})
		.Get(), &m_documentTitleChangedToken));
}

In those handlers is add_NavigationCompleted where it sets the zoom factor to 100%. I will bail out now and wait for the WebView2 team to offer insight.

@novac42
Copy link
Contributor

novac42 commented May 8, 2023

Thanks @ajtruckle for your input. @RickStrahl does the solution provided in #2451 address your problem?

@bradp0721
Copy link
Member

@RickStrahl it is intentional that WebView2 zooming does not match the browser because browser zoom applies to all tabs on the same site of origin and we didn't want zooming in one WebView2 to cause other WebView2's content to also zoom.

Extending your full browser example:

  • User zoom will zoom all tabs on the same web site. (i.e. zooming one tab on bing.com will cause all tabs on bing.com to be zoomed.)
  • Zoom level is remembered per web site (so refreshing preserves the zoom, but loading a new page results in zoom level reset to default).

For WebView2, we didn't think it made sense to apply the zoom based on the web site like the browser does. This would impact all the WebView2s for that user data folder. The extreme example here is Office applications which share a user data folder so that users get cookie/local state sharing. Imagine changing the zoom of a WebView2 in Word and you see content in PowerPoint get zoomed (because they are on the same web site). The Office team did not find that behavior desirable.

So instead of saving the user zoom per web site, the user applied zoom is only for the current page in the current WebView2 and is discarded on navigation. Any navigation resets the zoom factor to the default value of 1.0. Additionally, if the application itself sets the ZoomFactor property, then that is treated as changing the default value. Any user zoom is still reflected to the application through the ZoomFactorChanged event, but future navigations will reset the zoom factor to the application set default. This is documented here.

@RickStrahl
Copy link
Author

RickStrahl commented May 8, 2023

@novac42 - no it doesn't solve the problem primarily because the ZoomFactorChanged event on its own makes it very difficult to capture the ZoomFactor because the ZoomFactor is reset to the forced original value for any reload. IOW, you can't rely on tracking that value as a 'user value'. It's always the forced value after a reload which is not what you want if you're tracking it in any way.

@bradp0721 - I agree that the per site stickiness is not a good fit for a control, although I would say it's better than what you have now. The idea of always resetting to some default value on every new navigation feels counter intuitive especially since you are setting it at the control level. It makes it really hard to keep track of the user desired value because it constantly resets to a forced value.

It always seems wrong when a value is explicitly reset for some nebulous internal reasons that aren't either explicitly set or by initiated by a user.

The preferred behavior I would like to see is to have no explicit reset:

I set a value when the control is loaded, and then any user changes in the control are properly captured in ZoomFactorChanged - at that point I'm in control of how I want to handle the Zoom. I can decide to just leave it or choose to optionally set to my initial state, the user adjusted state (which persists) or any explict code change. But it puts me in control of that, not the control.

The current behavior is confusing because it neither matches browser behavior, nor what I would consider normal default behavior (which is set it and don't force it ever).

Currently you either accept the default behavior or you have to write hacky code like what I show above to do something different - because it's the only way to track the ZoomFactor value away from the forced reset value. IOW, you make everything difficult, except the default path. What I suggest has a different default behavior, but makes it easy to implement auto-reset to some fixed value behavior (ie. like the current implementation) or any other custom behaviors.

That said - I have my workaround, just feel like that shouldn't be necessary.

@RickStrahl
Copy link
Author

RickStrahl commented May 8, 2023

I see two ways this can be fixed:

  1. Change the behavior to be more natural (per me 😄)
  2. Add some indicator in the EventArgs that let me differentiate between user/programmtic or your RESET action

Still thing 1. is the way to go because it'll be the expected behavior IMHO.

Or do nothing and see tons of questions and complaints regarding how to fix the Zoom behavior 😄

@ajtruckle
Copy link

I keep it simple. I have 4 tabs so 4 zoom values in registry. If user changes zoom I update the view and the reg. when I activate a tab I set that zoom.

For me I technically have one view control and no tabs. These are a tab control on my dialog. I find this quite simple and works for me.

@RickStrahl
Copy link
Author

RickStrahl commented May 8, 2023

@ajtruckle - yeah that works as long as you only set the Zoom on startup.

But now let your user change the zoom and then allow them to refresh the page. They'll go back to the original Zoom level (or default of 1) that you set, not the zoom they set. That's where this problem comes in.

It's not one of those things that is huge issue especially depending on the application, but if you have tools that rely heavily on WebView behavior (in my case an editor and preview with 1000s of users) somebody is bound to be complaining about the non-sticky behavior and having to rejigger the zoom constantly. Heck I do myself, even if it's not often.

As said - I fixed this recently with the code I show above, and that can be used to customize Zoomfactor handling, but it feels that this is way harder than it needs to be for no good reason.

@ajtruckle
Copy link

@RickStrahl

But now let your user change the zoom and then allow them to refresh the page. They'll go back to the original Zoom level (or default of 1) that you set, not the zoom they set. That's where this problem comes in.

For me, if I set the zoom to 150% and then Refresh it re-creates the document and re-displays it, still at 150%. So I am probably doing custom actions in this scenario.

@pushkin-
Copy link

@ajtruckle Yeah I'm also having trouble reproing this. If I change zoom in a webview2 control and open devtools and location.reload() or just navigate to another site, my zoom is preserved.

@ajtruckle
Copy link

@pushkin- You are commenting on something that is over a year old from my point of view. It works for me by using the code I displayed. I won't bother fiddling with it now.

@Optimierungswerfer
Copy link

We also run into this issue with our application and had users complain about it.

@pushkin- You are commenting on something that is over a year old from my point of view. It works for me by using the code I displayed. I won't bother fiddling with it now.

@ajtruckle We also based our code on Marius Bancila's blog post, since we are an MFC application. Yet we still see this issue. Which is to be expected as none of his code deals with it. I am curious what it is that you seem to be doing so you do not see this issue.

As said - I fixed this recently with the code I show above, and that can be used to customize Zoomfactor handling, but it feels that this is way harder than it needs to be for no good reason.

@RickStrahl thank you very much for the workaround. It restores the expected behavior. Unfortunately, the workaround breaks the Ctrl+0 behavior to reset the Zoom to the default, because programatically changing the ZoomFactor property, sets the new value as the new default, as @bradp0721 explained here:

Additionally, if the application itself sets the ZoomFactor property, then that is treated as changing the default value.

To mitigate this, we extended @RickStrahl 's workaround to additionally intercept the AcceleratorKeyPressed event (we use C++/WinRT):

// Restore Ctrl+0 default zoom factor behavior, as the above breaks it, because setting ZoomFactor programatically also changes the default zoom factor... (see https://github.com/MicrosoftEdge/WebView2Feedback/issues/3459#issuecomment-1538818084)
m_pWebViewImpl->m_webView2Controller.AcceleratorKeyPressed([this](const CoreWebView2Controller& webView2Controller, const CoreWebView2AcceleratorKeyPressedEventArgs& args) {
  if (args.VirtualKey() == static_cast<uint32_t>(winrt::Windows::System::VirtualKey::Number0))
  {
    m_dZoomFactor = m_dDefaultZoomFactor;
    webView2Controller.ZoomFactor(m_dDefaultZoomFactor); // 1
  }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants