Thursday, February 28, 2013

Windows Store Apps, SVG, and HTML Interoperability

There are several reasons you may wish to show HTML content in your Windows Store app. You may have information that is updated frequently and makes the most sense to consume as HTML data. Your app may aggregate feeds that contain HTML content. In some cases you may be creating a native client that accesses an existing web-based application that you need to interoperate with. Fortunately, the WinRT has a control that addresses these needs: the WebView control.

It is important to understand what the purpose of the control is and how that may impact how you architect your app. The control itself is not intended to be a complete replacement for a browser. Indeed, Microsoft has made it clear that if you try to get an app approved in the store that tries to solely interact with a web-based app or simply act as a web browser, it will be rejected [1-Ten Things You Need to Know about WebView]. There are several limitations built into the control that prevents many scenarios.

The control uses the Internet Explorer 10 engine. It does not support advanced features in HTML5 including caching, indexed databases, programmatic access to the clipboard, or geo location. It supports the same version of the Document Object Model (DOM) that is used by Windows Store apps written in JavaScript [2 - Document Object Model (DOM) (Windows)]. There is no support for any type of plug-in or ActiveX extension, including Flash, Silverlight, or embedded PDF documents.

There is full support for enhanced protocol handling [3 - Auto-launching with file and URI associations]. This provides support for things like custom protocols that allow you to access resources embedded in your app as well as links that will automatically launch the program associated with them. You can generate a web page that, when viewed, not only provides content served from within your app, but also allows the user to open those resources using their preferred app (for example, a resource with a .pdf extension will open with the preferred reader app).

The control is also unique because it cannot be covered by other controls. It completely takes over the pixels defined for its area. Any controls that you attempt to place in front of the WebView control will be cropped. If you must overlay the page with controls, you can capture the current view using a special WebViewBrush and use that brush to render a surface.

The final and perhaps most important feature of the WebView control is the ability to interoperate with your Windows Store app. Your app can communicate with the web page by invoking JavaScript functions and passing in parameters. The page can also interact with your app by calling a method on an object that WinRT creates in the DOM. This allows you to create some truly interactive experiences between web content and your Windows Store app.

The WebViewExamples project contains several examples of working with HTML and JavaScript. MainPage.xaml defines a WebView control named WebViewControl and a brush named WebBrush. When the page is loaded, a handler is registered to the LoadCompleted event on the WebView. This will fire any time a new resource (whether it is a string literal or a web page) is loaded. It will force the brush to refresh and show a dialog indicating the content that was loaded.

private async void WebViewControlLoadCompleted(object sender, NavigationEventArgs e)
{     
WebBrush.Redraw();
     var url = e.Uri != null ? e.Uri.ToString() : "Text Content";
     var popup = new MessageDialog(url, "Content Loaded.");
     await popup.ShowAsync();

}

The first button triggers a straightforward page load.

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
     this.WebViewControl.Navigate(new Uri(JeremyBlog)); }

The page is my main blog page. You’ll find it loads and renders fine. You can also click on links and navigate just as if you pulled the page up in a web browser. There are no forward or backwards buttons and there is no place to enter a new address so you are stuck following the links provided by the control. Notice that the dialog fires every time you navigate to a new page – you could easily use this feature to inspect where the user ended up and hide the control or reset it if needed.

The second button invokes a specific page as a mobile client by writing a user agent that mimics an iPhone. My blog will detect that a mobile device is accessing the page and redirect to a mobile-friendly page. In this mode you may see several errors thrown by the debugger. This is because the WebView control will throw an error any time it encounters a JavaScript error or an issue parsing the DOM. You can turn off the JavaScript errors by going into the debugger settings and un-checking Script under the Just-In-Time tab. Other errors you have to muscle through and just select No – on my machine the mobile page generates four errors I must acknowledge before accessing the page. If you run the app outside of the debugger, it will swallow these errors and run normally.

The only way to update the user agent that the WebView control uses is to use some hacks that will prevent your app from making it into the app store. For those hackers in the audience, you’ll need to p/Invoke urlmon.dll and call UrlMkSetSessionOption with option 0x10000001 and your user agent string prior to navigating with the control. A more direct approach is to download the page yourself and pass it to the control as text. The steps are shown below.

private async void ViewMobile_OnClick(object sender, RoutedEventArgs e)
{
     var handler = new HttpClientHandler { AllowAutoRedirect = true };
     var client = new HttpClient(handler);
     client.DefaultRequestHeaders.Add("user-agent", MobileUserAgent);
     var response = await client.GetAsync(new Uri(JeremyYogaPost));   
response.EnsureSuccessStatusCode();
     var html = await response.Content.ReadAsStringAsync();
this.WebViewControl.NavigateToString(html);
}

The example sets up a client that accepts redirects (in the case of the blog post reference, it will redirect to a mobile version), then creates a client and sets the user agent to mimic a mobile device. The page is requested and the code ensures a successful response was obtained before loading the content of the response and sending it to the WebView control.

This technique is common for requesting content in a format that is suitable for viewing within your app (mobile content tends to have less chrome so it is easier to process). You can also parse and analyze the content prior to presenting it to the end user. This allows you to strip unwanted tags or even inject your own JavaScript and content as needed prior to the page being displayed.

In some apps you may wish to include embedded HTML resources for your apps to display. The third button demonstrates this technique. It also shows that the rendering engine has full support for Scalable Vector Graphics (SVG) [4 - W3C SVG Working Group], which can be useful if you need to integrate charts or vector-based graphics in your application and have existing implementations using SVG. In the example app, there is an embedded HTML file named Ellipse.html in the Data folder.

Referencing this resource is simple and straightforward. The special ms-appx-web protocol provides a path to the WebView control that is embedded in the store app. You’ll notice it uses three forward slashes and then provides a path to the resource in the app from the root.

private void ViewSvg_OnClick(object sender, RoutedEventArgs e)
{
     this.WebViewControl.Navigate(new
          Uri("ms-appx-web:///Data/Ellipse.html")); }

You can also embed static text (or dynamic text that you build and generate prior to passing into the control). The fourth button triggers the load of a string literal. The literal contains the text for an HTML document and includes various header tags, an embedded image (the logo of the app itself), and a hyperlink that demonstrates you can navigate to embedded content or external content. If you follow the link to the external website, you’ll see that the base URL is consistently reported as static text because that’s how the original document was loaded.

private void ViewString_OnClick(object sender, RoutedEventArgs e)
{
     this.WebViewControl.NavigateToString(HtmlFragment); }

The final button demonstrates how your app can interoperate with the web. To show this, I included a JavaScript function in the template of my blog. The function is just a few lines of code. If you load any page from my blog and view the source, you can search for “supersecret” to locate the function:

function superSecretBiographyFunction(subPath) {
   window.location.href="http://csharperimage.jeremylikness.com/" + subPath;
}

The function is usually not called, but that will change if you load one of the blog pages using the example app and click the last button. The code snippet below shows the implementation. The InvokeScript method is used to call the function. You can pass in null for a function that takes no parameters, otherwise you can pass in an array of values. In this case the path to my biography is passed in, and the page picks it up and navigates to the path. Note that you can call JavaScript on any type of page the control rendered, including embedded resources or literals you’ve passed in.

private async void CallJavaScript_OnClick(object sender, RoutedEventArgs e)
{
     MessageDialog popup = null;
     var parameters = new[]
                          {
                                 "p/biography.html"
                          };
     try 
{
        this.WebViewControl
.InvokeScript("superSecretBiographyFunction", parameters);
    }
    catch (Exception ex)
     {
        popup = new MessageDialog(ex.Message, "Unable to Call JavaScript.");
     }
     if (popup != null)
     {
         await popup.ShowAsync();
     } }

An attempt to invoke a function on page that doesn’t have that function will result in an exception. The code captures the very user-unfriendly exception and displays it for you. The pattern is important to note because it is not possible to use the await keyword from within a catch block. The workaround is to capture any necessary information needed in the catch, then await the dialog to display the information once the block is exited.

Although sending information to a page is nice, wouldn’t it be great if you could receive information back? This scenario is possible and you can in fact create apps that have two-way conversations with web pages. If you searched for the source snippet above, you may have also noticed this block of code:

if (document.title==='C#er : IMage: Synchronous to Asynchronous Explained') {
if ((typeof (window.external) !== "undefined") && (typeof (window.external.notify) !== "undefined")) {
      window.external.notify(new Date().toLocaleString());
   }
}

I would not normally do a string comparison against a topic title to trigger application logic, but I can get away with it here because I own the blog and am not planning on changing the title. When the user navigates to a specific topic inside a WebView control, the web page itself will actually send a message to the Windows Store app that contains the localized date and time. The communication between the web page and the Windows Store app is illustrated here:

htmlwinstoreinterop

The function first checks for the existence of the window.external object. This is provided to the JavaScript runtime by Internet Explorer to provide access to the object model of the host [5 - External object (Internet Explorer)] (in this case, the host is the WebView control). The control in turn exposes the notify method to the runtime. A call to this from the web page with a single string parameter will raise the ScriptNotify event on the WebView control.

The example app simple stores the value that is passed to a local variable.

private void WebViewControlScriptNotify(object sender, 
Windows.UI.Xaml.Controls.NotifyEventArgs e) {
     this.message = e.Value; }

Once the entire web page is loaded, the LoadCompleted event checks for this value and displays it when present.

if (string.IsNullOrEmpty(this.message))
{
     return; } popup = new MessageDialog(
     this.message,
      "The Blog Has Spoken!"); this.message = string.Empty; await popup.ShowAsync();

To see this in action, use the timeline on the right side of my blog to navigate to the year 2012, the month August, and the title “Synchronous to Asynchronous Explained.” The result should be two pop-ups, one to notify you when the page has loaded and a second that declares the blog has spoken and displays the date. This closes the loop and demonstrates how Windows Store apps can load, display, and interact with HTML-based content.

This post just scratches the surface of what is possible but hopefully provides you with an in-depth demonstration of how you can embed HTML, SVG, and even JavaScript in your Windows Store apps.

Full Source Code

Side-Load Package