WebAssembly in Action

Author of the book "WebAssembly in Action"
Save 40% with the code: ggallantbl
The book's original source code can be downloaded from the Manning website and GitHub. The GitHub repository includes an updated-code branch that has been adjusted to work with the latest version of Emscripten (currently version 3.1.44).

Wednesday, September 12, 2012

Windows 8: Windows Store apps and the WebView control


I recently started looking into creating a simple Windows 8, Windows Store app, that would contain a browser window and a setting somewhere to allow the URI to be modified.

The app itself is quite simple in concept so I figured it would be a breeze to implement for Windows 8.

To display a web page I needed to use a WebView control and I simply set the Source property to the desired Uri.


The Settings Flyout

After successfully building my solution, and seeing the web page displayed, I decided it was time to move on to building the option to allow a user to modify the URI.

To try and keep things consistent with other Windows Store apps, I decided that it would be best to try and integrate the settings window into the Settings Charm of Windows.

I did some research and integrating into the Settings Charm seemed pretty straightforward in that you simply add an event handler for the CommandsRequested event of the Settings Charm and when the event is called, create your SettingsCommand callback.


Showing a Settings flyout seemed straightforward but I couldn't seem to get it to display.

While searching the internet, looking for articles or forum posts that might shed some light on my issue, I ran across the Callisto open-source toolkit, created by Tim Heuer, that offers boilerplate code for Windows Store apps.

You can find Tim's Callisto blog here: http://timheuer.com/blog/archive/2012/05/31/introducing-callisto-a-xaml-toolkit-for-metro-apps.aspx

The GitHub repository for the Callisto toolkit can be found here: https://github.com/timheuer/callisto

To install the Callisto package into your project via NuGet (https://nuget.org/packages/Callisto), you can use the following command:

PM> Install-Package Callisto


Unfortunately, even with the Callisto toolkit, my settings flyout still wasn't being displayed.

When I was trying to create the settings flyout myself, before I discovered Callisto, I needed to set the height of the flyout so I started to wonder if perhaps the WebView control's VerticalAlignment setting of Stretch was the issue since I wasn't setting an explicit height value.

I set the height of the WebView control to 700 pixels and, when I ran my app, I discovered what the issue really was.

It turns out that the settings flyout was being displayed all along but the WebView control was hiding it because I can now see it being displayed behind the WebView control.


WebViewBrush

After a bit more research, I came to discover that you need a workaround with the WebView control when displaying a flyout (unfortunately, this reminds me of IE 6 and how Select objects would show through a floating div).

The following is the suggested workaround for displaying a flyout over a WebView control:
  • Add a Rectangle object to your view's XMAL with the same dimensions as your WebView control
  • Just before you show the flyout, create a WebViewBrush with the current contents of the WebView control (basically a screen shot)
  • Apply the brush to the Rectangle control
  • Hide the WebView control
  • Display the flyout
  • When the flyout closes, redisplay the WebView control
  • Clear the brush from the Rectangle control

I modified my view to use the workaround and my settings flyout now displays which I'm very happy about.

The main issue that I see now is that there is a noticeable flicker when switching to the WebViewBrush.

My initial thought, since the Redraw method of the brush is asynchronous, was that the WebView control was being hidden a fraction of a second before the brush had time to finish loading resulting in the background of the view being briefly visible causing the flicker.


I tried everything I could think of to get rid of the flicker including making the WebViewBrush a member variable and constructing it during the view's constructor.

I even tried placing the Redraw call before the settings flyout code creation to try and give the brush a few more milliseconds to load but the flicker remained.


async Task.Delay

Something I was curious about was if causing the app to sleep, in between the Redraw call of the brush and the hiding of the WebView, would help.

As it turns out there is no Thread.Sleep method available in a Windows Store app but I did find a workaround using an async Task.Delay(5000) call (I used 5000 milliseconds just so that it was obvious that the sleep call worked)

Much to my pleasant surprise, the flicker disappeared!


When I looked closer at the code, a thought crossed my mind...

I had both the WebViewBrush’s Redraw method as well as the Rectangle's Fill property being set before the delay.

What if the flicker wasn't because of the Redraw method of the brush after all?

What if the issue is with the Fill property of the Rectangle object?

I moved the rectangle's Fill call to after the delay and the flicker returned which indicates to me that the issue is not the brush but rather the Rectangle’s Fill property.


As I started thinking about what the issue might be, a thought occurred to me...

What if this is behaving similar to how the UI thread behaves in a web browser?

In a web browser there is only the one UI thread for a window so when you want to update the UI while processing, your UI request gets added to the end of the list of things that the window plans to do once the currently executing code completes.

Typically, if your code might take a few seconds to complete, you would want to display a processing indicator of some sort while your code executes (so that the user doesn't think that the page froze).

If you simply tell the UI processing control to display and then start processing, the UI control won't display until your code completes since the UI request is queued up for execution next by the browser window and your code is what it's currently working on.

To get around this UI update issue, in a web browser, you tell the UI control to display which adds that item to the end of the window's queue and then you set a timeout to call the function that will do the actual processing which puts the processing function on the queue just after the UI update.

When the original function exits, the UI is updated and then the processing function is called.

Based on the behavior I'm seeing, I believe the Windows Store app is using a similar technique to the UI thread of a web browser window


If I set the Rectangle's Fill property with the WebViewBrush and then add an async delay of 1 millisecond before proceeding to display the settings flyout, there is no flicker!

I still saw the flicker once in a while when closing the Settings flyout via the back button and then redisplaying the flyout really quick. Increasing the delay to about 100 milliseconds seems to improve that scenario.


Example Code

The following is an example of the XAML needed:
<Grid>
<WebView x:Name="wvBrowser" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />

<Rectangle x:Name="rectWebViewBrush" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"></Rectangle>
</Grid>

The following is an example of the source code needed to show the Settings flyout when dealing with a WebView control:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
SettingsPane.GetForCurrentView().CommandsRequested += MainPage_CommandsRequested;
}

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
SettingsPane.GetForCurrentView().CommandsRequested -= MainPage_CommandsRequested;

base.OnNavigatingFrom(e);
}


// Called by the Settings charm to find out what Settings links
// to display and the code to call when clicked.
void MainPage_CommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
{
// Set up the Link for the Settings charm
SettingsCommand cmdSettingsOptions = new SettingsCommand("cmdSettingsOptionsLabel", "Options", (x) =>
{
// Get a brush from the WebView's content (basically,
// a screen shot)
WebViewBrush wvbBrush = new WebViewBrush();
wvbBrush.SourceName = "wvBrowser";
wvbBrush.Redraw();

// Fill the Rectangle object with the brush
rectWebViewBrush.Fill = wvbBrush;

// Show the settings flyout
ShowSettingsFlyout();
});

// Add our Setting link to our applications list of settings
args.Request.ApplicationCommands.Add(cmdSettingsOptions);
}


// Creates and displays the Settings flyout:
async void ShowSettingsFlyout()
{
// Give the Rectangle a chance to refresh so that we
// don't have flicker
await Task.Delay(100);

// Create the Settings flyout and display it (this is using
// the Callisto open-source toolkit). Additional attributes
// can be set like the background color and header brush
// to better represent your app's look and feel
SettingsFlyout settings = new SettingsFlyout();
settings.FlyoutWidth = Callisto.Controls.SettingsFlyout.SettingsFlyoutWidth.Narrow;
settings.HeaderText = "Options";

// Intercept the setting's closed event so that we can
// switch back to the WebView control
settings.Closed += settings_Closed;

// Our UserControl derived class that holds the controls
// for our settings
settings.Content = new SettingsOptionsContent();

// Switch to the WebViewBrush and then show the
// settings flyout
SwitchToWebViewScreenShot(true);
settings.IsOpen = true;
}


void SwitchToWebViewScreenShot(bool bSwitchToScreenShot)
{
// If we're to show the screen shot then...
if (bSwitchToScreenShot)
{
// Hide the WebView control (MainPage_CommandsRequested
// has already set the rectangle's fill with a screen shot of the
// contents of the WebView control)
wvBrowser.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
}
else // We're to show the WebView again...
{
// Show the WebView control and remove the WebViewBrush
// from the Rectangle
wvBrowser.Visibility = Windows.UI.Xaml.Visibility.Visible;
rectWebViewBrush.Fill = new SolidColorBrush(Colors.Transparent);
}
}


// Called when the Settings flyout closes
void settings_Closed(object sender, object e)
{
// Switch back to the WebView control rather than the screen shot
SwitchToWebViewScreenShot(false);
}



In Summary

WebView controls don't allow a flyout to appear over it and require the use of a WebViewBrush to basically display a screen shot while the flyout is displayed.

The filling of a Rectangle object with a WebViewBrush, and immediately hiding the WebView control, introduces a flicker that can be circumvented by interrupting the UI's processing flow by adding an async delay before hiding the WebView control.

Windows Store apps don't support Thread.Sleep but you can accomplish a similar effect by using 'async Task.Delay'


When I was researching the WebViewBrush flicker issue, several forum posts indicated that there was a bug filed for it so there is potential that the flicker will not be an issue forever.

In the mean time, however, I hope this helps.


Download the Source Code

A download of the project (C# and built using Visual Studio Express 2012 for Windows 8) can be found in the following location:
https://github.com/downloads/dovicoapi/DOVICOTimerForWindowsStore/DOVICOTimerForWindowsStore.zip

4 comments:

  1. Thanks for sharing your experiences. Any chance of posting the sample project?

    ReplyDelete
  2. I've zipped up my project (C# and built using VS Express 2012 for Windows 8) and added it to the following location: https://github.com/downloads/dovicoapi/DOVICOTimerForWindowsStore/DOVICOTimerForWindowsStore.zip

    Note: This project is still a work in progress so it may not comply with all of the Windows Store restrictions yet. I will be updating this project on github as it proceeds.

    ReplyDelete
  3. The app has now passed certification. The download complies with the Windows Store restrictions.

    ReplyDelete
  4. How I can ClearValue of WebView it is not working I had tried with many option but still not working
    webViewPlanner.ClearValue(WebView.SourceProperty);

    ReplyDelete