Customizing the Windows Phone 8 WebView

I’m currently trying to make my first Windows Phone app (a bit late to the party I know).
One of the requirements I have is showing HTML, which I fetch from a REST service, in the app.

But this was more challenging than expected.

To show HTML I need to use a WebView.
But there is no databinding support for HTML strings (as far as I know?), the WebView should scale with the content inside it and I should be able to scroll inside my app view (not inside my WebView) so I need a way to disable the scrolling ability in the WebView.

It’s possible that the content of my WebView is larger than the available screen estate but that’s not a problem because I can add a ScrollViewer which allows me to … scroll (tadaa!).

Databinding support for HTML strings

First about the databinding support for HTML strings.
I got this solution from another blog, but unfortunately I forgot which one and can’t find it anymore in my history… I’ll add the author/link once I found it.

The WebView class itself is sealed so I can’t inherit from it.
Another possibility is to register a new attribute for the WebView.

First a look at the code:

public static string GetHtmlContent(WebView view)
{
    return (string)view.GetValue(HtmlContentProperty);
}

public static void SetHtmlContent(WebView view, string value)
{
    view.SetValue(HtmlContentProperty, value);
}

public static readonly DependencyProperty HtmlContentProperty =
    DependencyProperty.RegisterAttached(
        "HtmlContent", 
        typeof(string), 
        typeof(WebViewExtensions),
        new PropertyMetadata(null, OnHtmlContentChanged)
    );

private static void OnHtmlContentChanged(DependencyObject sender,
    DependencyPropertyChangedEventArgs e)
{
    var webView = sender as WebView;
    if (webView == null)
        throw new NotSupportedException();

    if (e.NewValue != null)
    {
        webView.NavigateToString(e.NewValue.ToString());
    }
}

I’ve added this snippet in a static class called WebViewExtensions because additional functionality will be added later on.
You can now easily use the attribute in XAML:

<WebView mycustomnamespace:HtmlContent="{Binding MyProperty}" />

Ok that’s one down.

Resize the WebView according to the content

I used a solution from Jason Poon, but I needed to modify it a little bit to make it work.
The value you get from the JavaScript is in pixels. The unit of the Height property of the WebView is by default in DIP (Device Independent Pixels) which is 1/96th inch according to the documentation of Microsoft.

So before passing the value to the property we need to do a little formula.

var heightString = await webView.InvokeScriptAsync("var height = document.getElementById('content').clientHeight; return height.toString();", new string[0]); 

int height; 
if (int.TryParse(heightString, out height)) {     
    int dipHeight = ((int)(height * 96) / (int)DisplayInformation.GetForCurrentView().RawDpiY);     
    webView.Height = dipHeight; 
}

Because we’re calculating the height we only need the value of the Y-axis.

Unfortunately, we’re not done yet.

When we want to scroll the view we’re still activating the scroll in the WebView.
This is somewhat of a hack, but it seems to work quite well.

The idea is to add a transparent layer on top of the WebView.

<ScrollViewer x:Name="LayoutRoot">
    <StackPanel>
        <Grid>
            <StackPanel>
                <WebView Height="100" mycustomnamespace:WebViewExtensions.HtmlContent="{Binding MyProperty}" />
            </StackPanel>
            <Grid Background="Transparent" Height="Auto" />
        </Grid>
    </StackPanel>
</ScrollViewer>

The touch events are now passed on to the Grid which activates the ScrollViewer.

That’s it! Let’s recap with the complete code!

public static class WebViewExtensions
{
  public static async void ResizeToContent(this WebView webView)
  {
      var heightString = await webView.InvokeScriptAsync("var height = document.getElementById('content').clientHeight; return height.toString();", new string[0]); 
      int height;
      if (int.TryParse(heightString, out height))
      {
          int dipHeight = ((int)(height * 96) / (int)DisplayInformation.GetForCurrentView().RawDpiY);
          webView.Height = dipHeight;
      }
  }

  public static string GetHtmlContent(WebView view)
  {
      return (string)view.GetValue(HtmlContentProperty);
  }

  public static void SetHtmlContent(WebView view, string value)
  {
      view.SetValue(HtmlContentProperty, value);
  }

  public static readonly DependencyProperty HtmlContentProperty =
      DependencyProperty.RegisterAttached(
      "HtmlContent", typeof(string), typeof(WebViewExtensions),
      new PropertyMetadata(null, OnHtmlContentChanged));

  private static void OnHtmlContentChanged(DependencyObject sender,
      DependencyPropertyChangedEventArgs e)
  {
      var webView = sender as WebView;
      if (webView == null)
          throw new NotSupportedException();

      if (e.NewValue != null)
      {
          webView.NavigateToString(e.NewValue.ToString());
      }
  }
}

It feels somewhat like applying different hacks together but it seems to work quite well!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s