A Primer on WebView.NavigateToLocalStreamUri
Scenario
A forum post recently asked about navigating to a page that that contains images that are accessed using the ms-appdata:/// URL protocol. His problem was that he'd placed images in his application's local folder and wanted to use WebView.NavigateToString to show some dynamically-generated HTML content that referenced these images. However, when loading the content in the WebView, the HTML would display, but the referenced images would not appear.
Background
A common scenario for Windows Store apps is the use of WebView to view locally-hosted content. This is a terrific way to reuse data that has been previously generated for the web in your new Store app, or a way that you can dynamically generate UI in your app and display it - HTML is pretty easy to build.
Since the release of Windows 8, WebView has supported the method NavigateToString which allows the user to load a string of HTML as a webpage for display. A major drawback of this method is that it cannot load referenced resources; that is, objects such as included JavaScript won't work, and images that are referenced as local content (without a fully-qualified domain name) cannot load, as the content which is trying to load them does not have a base URI itself. Here’s an example of why this fails:
Let’s use the same HTML page loaded into WebView from two places:
<html>
<body>
Picture of cat <br> <img src=”cat.jpg” alt=”cats”>
<body>
</html>
What you should note here is that there are two URIs in play here:
Using this WebView call:
MyWebView.navigate("https://MyWebServer/My.html”);
the WebView attempts two loads:
1. The HTML content – from https://MyWebServer/My.html.
2. The source of the image (cat.jpg) – from https://MyWebserver/cat.jpg
The WebView understands that the location of the embedded image is the same website as the page itself, and we end up with the proper output:
However, when we make a WebView load the content like this:
String MyHTMLString = “<html><body>Picture of cat<br><img src=\”cat.jpg\” alt=\”cat\”><body></html>”;
MyWebView.NavigateToString(MyHTMLString);
The WebView again tries to make two loads:
1. The HTML content – from the string itself.
2. The source of the image (cat.jpg) – from... nowhere. There is nowhere to load this from. We end up with this:
Solution
Windows 8.1 introduces the method NavigateToLocalStreamUri. The idea behind this is that you may navigate to locally generated HTML, and also give the WebView a way to determine where locally referenced content can be found. I posted a solution using this technique in the forum. The example code is not long, but is rather cryptic, so I want to break it down to explain exactly what is required to make this happen:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<WebView x:Name="MyWebView" Width="500" Height="500"/>
</Grid>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
Uri MyUrl = MyWebView.BuildLocalStreamUri("MyContent", "/my.html");
StreamUriWinRTResolver MyResolver = new StreamUriWinRTResolver();
MyWebView.NavigateToLocalStreamUri(MyUrl, MyResolver);
}
}
public sealed class StreamUriWinRTResolver : IUriToStreamResolver
{
public IAsyncOperation<IInputStream> UriToStreamAsync(Uri uri)
{
if (uri == null)
{
throw new Exception();
}
string path = uri.AbsolutePath;
return GetContent(path).AsAsyncOperation();
}
private async Task<IInputStream> GetContent(string URIPath)
{
try
{
Uri localUri = new Uri("ms-appdata:///local/content" + URIPath);
StorageFile f = await StorageFile.GetFileFromApplicationUriAsync(localUri);
IRandomAccessStream stream = await f.OpenAsync(FileAccessMode.Read);
return stream.GetInputStreamAt(0);
}
catch (Exception)
{
throw new Exception("Invalid path");
}
}
}
There are four important steps to the code that allows the WebView to perform the navigation.
Uri MyUrl = MyWebView.BuildLocalStreamUri("MyContent", "/my.html");
The BuildLocalStreamUrimethod creates a unique URI for the content to which you want to navigate. At runtime, the value of the Uri is (in my app) is ms-local-stream://c499f7b9-5074-498c-864d-c334b96c3eb8_4d79436f6e74656e74/my.html.The "MyContent" parameter passed into the method helps generate the GUID part of the URI, while the actual contact you wish to browse to is indicated by the second parameter.
StreamUriWinRTResolver MyResolver = new StreamUriWinRTResolver();
The NavigateToLocalStreamUri gets its power from the StreamUriWinRTResolver class, which derives from IUriToStreamResolver. Inside the WebView, the loaded HTML has an img tag which points to an image which is not fully-qualified. However, since the page has a LocalStream URI (which is not the case with NavigateToString), this URI is passed as the root of the image, and the WebView knows to call into the resolver to figure out how to process the request.In our specific case, we request the data from the ms-appdata://local/content folder. However, we could parse the request, and decide that image requests could come from a remote server via an HTTPClient call, refer to an image somewhere else in the application, or we could generate them dynamically - the options are unlimited as to how you want to fulfill the request. However, what you must do is return a stream which is appropriate for the request.
MyWebView.NavigateToLocalStreamUri(MyUrl, MyResolver);
This line pairs the StreamUri with the resolver, and asks the WebView to perform the navigation. After this call is made, every locally-referenced URI inside the HTML page will force a call to the resolver's UriToStreamAsync method, where its stream will be returned to the calling WebView for rendering.
Now when we run the code, the WebView tries to load the content like this:
-
The HTML content –
- The LocalStreamUri that is generated by WebView: ms-local-stream://c499f7b9-5074-498c-864d-c334b96c3eb8_4d79436f6e74656e74/my.html is fed to the Resolver.
- The Resolver removes the protocol and server part of the URI and changes it to be a reachable URI in the application’s local folder: ms-appdata:///local/content/my.html
The source of the image (cat.jpg) –
- The LocalStreamUri that is generated by WebView: ms-local-stream://c499f7b9-5074-498c-864d-c334b96c3eb8_4d79436f6e74656e74/cat.jpg is fed to the Resolver.
- The Resolver removes the protocol and server part of the URI and changes it to be a reachable URI in the application’s local folder: ms-appdata:///local/content/cat.html
Now we see the cat again:
Conclusion
I hope this brief overview of the WebView.NavigateToLocalStreamUri allows yo to understand why it’s necessary to use it, and explains what’s going on behind the scenes.
Comments are encouraged, and feel free to tweet to me at WinDevMatt, or use my team’s handle WSDevSol.
Comments
Anonymous
July 05, 2014
I blending a RSS Reader,But I hope load the specified page content,I need to modify which parts of the code,if you have a idea,please contact me,my email:zangxx66@gmail.comAnonymous
September 26, 2014
Can we include headers in the stream (such as content type or cookies) as would be the case if the content was served by a real http server? Also, how could we intercept those headers from the request in OnNavigatedTo ? Any ideas?Anonymous
July 21, 2015
I want to return an InMemoryRandomAccessStream with my definition of the method "private async Task<IInputStream> GetContent(string URIPath)", but I alway I get an error. (with Windows Phone 8 app and Windows Phone Store app) How can I fix this issue. Maybe it's the same problem as this one: www.ciiycode.com/.../systeminvalidcast-exception-windows-runtime @Matt Small It would be great if you could explain how I can fix this problem.Anonymous
January 12, 2016
Hi, I have a problem here and i need your help. I loaded a html start page. It include some javascript files. The page can load these external js files properly. The start page navigate to another page ( still in same local stream uri), i call it is secondary page. The secondary page include some javascript files too. But it can not load any js file. I dont know why, maybe it blocked by the browser because main page is the start page. Do you have any ideas about this ?