Something's Missing from the WebBrowser Control
Published on Monday, January 30, 2012 2:40:09 PM UTC in Programming
Sometimes seemingly simple things are the most expensive to achieve. One such thing involves the Windows Phone WebBrowser control that allows you to show arbitrary web content embedded in your application.
The problem I ran into is that the control does not show any scrollbars when the content is larger than the visible area. This is a bit of a surprise, because the built-in browser of the phone does this nicely:
This means that your user has no indication of the total content length or the current scroll position when you show HTML content in your application, which is a bit unfortunate. In the following I talk about the various attempts and ideas I tried to fix this; if you're only interested in a possible work around, you can skip to the end of the post.
First Attempts
Some of the obvious things you might try in that situation if you're familiar with Windows Phone or Silverlight development is to set the VerticalScrollBarVisibility attached property to the appropriate value, or to wrap the whole WebBrowser control in a ScrollViewer element and similar thing. None of these methods will result in anything usable.
When you search around the web you will find that others are struggling with this too, and there doesn't seem an out of the box solution to it. In this topic in the MSDN forums Mark Chamberlain even states:
"I verified internally that the WebBrowser control by its very nature does not support the vertical scrollbar, and the mouse events are not exposed that otherwise would allow you to roll your own scrolling mechanism."
And, some time later:
"If possible you should consider architecting your application so you don't need a vertical scrollbar in WebBrowser."
Uhm… okay…? To be honest, I found that answer a bit disappointing, so I dug a bit deeper.
Ideal Solution
The best solution to the problem would be if you could simply tweak some of the involved elements so the control works with arbitrary content. To get an idea of the internals, I first took a look at the visual tree of a sample app that has the WebBrowser control on a page:
As you can see, the tree is pretty simple for the WebBrowser control: "TileHost" seems to be the native IE component, whereas the other interesting control here is "PanZoomContainer" – this one seems to handle all the user interactions and looked like a promising place to start. Even more when you take a look at the element in the debugger:
As you can see, this control maintains all the relevant information to present the current visible area to the user, and it would be perfect to implement an own, custom scrolling visualization. For example, using the information from the content size and current scale (or the scrollable height) and view port would be sufficient to calculate the values and offset of a custom scroll bar at any given time.
Unfortunately, the PanZoomContainer control is internal, and due to the security restrictions of the platform that means you cannot access any of these values with Reflect-Fu, even the public ones. :(
By the way, if you dig even further you'll notice that this container internally already maintains both a horizontal and vertical scroll bar too (_scrollH/_scrollV). These controls are ready to go and seem to have the correctly calculated and updated values at runtime – why they are not exposed or used, I don't know.
The Work Around
The other way to get information about the current scroll offset and total content size is from within the web page that is shown in the WebBrowser control itself. This is nothing new and involves some basic JavaScript. The reason why this is problematic is that you need to have control over the content you're loading into the control (either server-side, or you need to use locally stored content).
In my case I was using local content anyway, so I injected some JavaScript into the code that I display in the WebBrowser control. A simplified version could look something like this (put in the head tag, for example):
function initialize() {
window.external.notify("scrollHeight=" + document.body.scrollHeight.toString());
window.external.notify("clientHeight=" + document.body.clientHeight.toString());
window.onscroll = onScroll;
}
function onScroll(e) {
var scrollPosition = document.body.scrollTop;
window.external.notify("scrollTop=" + scrollPosition.toString());
}
window.onload = initialize;
The idea is that when the page loads, we want to notify our hosting phone app about both the height of the visible area and the available scroll height. Then, whenever the current scroll position changes, we want to pass on that information too.
On the Silverlight side of things, first of all we have to add a separate ScrollBar control next to the WebBrowser in XAML, to fake the scrolling visualization:
<Grid x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<phone:WebBrowser x:Name="ContentWebBrowser"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0 0 5 0"
IsScriptEnabled="True" />
<ScrollBar x:Name="DisplayScrollBar"
Orientation="Vertical"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Minimum="0"
Maximum="100"
Value="0" />
</Grid>
Do not forget to set the "IsScriptEnabled" property of the WebBrowser control to true.
Then we can hook the "ScriptNotify" event that is raised every time JavaScript invokes "window.external.notify" (see above). In this event handler we can inspect the value that is passed to Silverlight and set up the fake ScrollBar accordingly:
// e.g. in the constructor, or in XAML
ContentWebBrowser.ScriptNotify += ContentWebBrowser_ScriptNotify;
...
private int _visibleHeight = 0;
private int _scrollHeight = 0;
private void ContentWebBrowser_ScriptNotify(object sender, NotifyEventArgs e)
{
// split
var parts = e.Value.Split('=');
if (parts.Length != 2)
{
return;
}
// parse
int number = 0;
if (!int.TryParse(parts[1], out number))
{
return;
}
// decide what to do
if (parts[0] == "scrollHeight")
{
_scrollHeight = number;
if (_visibleHeight > 0)
{
DisplayScrollBar.Maximum = _scrollHeight - _visibleHeight;
}
}
else if (parts[0] == "clientHeight")
{
_visibleHeight = number;
if (_scrollHeight > 0)
{
DisplayScrollBar.Maximum = _scrollHeight - _visibleHeight;
}
}
else if (parts[0] == "scrollTop")
{
DisplayScrollBar.Value = number;
}
}
This works…
… but with an annoying limitation: As long as the user pans around in the WebBrowser control, the JavaScript "onScroll" event is not raised. Not even the "scrollTop" value is updated during that time (so you cannot use a different approach like a timer either). Only when the panning comes to a halt is the new "scrollTop" value of the page updated. This means that as long as scrolling is in progress, you don't see any updates of the scrollbar position in your app, which is kind of irritating and inconsistent compared to the normal behavior we know from the platform.
Conclusion
We have yet to find a way to make this work consistently and in a better way. My current work around is not perfect and slightly irritating in its behavior, but despite several other attempts I couldn't find a better alternative. One thing I haven't tried yet is to intercept user input for the control and make use of that. But without access to the internal state of at least the zoom/scaling that doesn't seem very promising anyway. If you have additional thoughts or can come up with something better, please let me know.
Tags: ScrollBar · WebBrowser · Windows Phone 7