Introduction
Microsoft did not give a Promise or callback type of wrapper for the pattern of navigateToLocalStreamUri
which requires an interface implementation which is not possible in WinJS/JavaScript. Hopefully, in the future, Microsoft will implement this interface which has been around since Windows 8.1. In the mean time, this is an attempt to show how it can be done with a helper wrapper in native C++ or C++/CX.
Background
Knowledge of native C++ or C++/CX, WinRT, COM, threading and threading model, WinJS and JavaScript as well as if one would choose to do some translation of the C++/CX code, then VB.NET/C# also could implement this. This is a bit more advanced than typical web programming.
Using the Code
The code requires IDL for native C++ and if one were to translate to VB.NET/C# or choose to only use C++/CX since the native C++ is advanced and largely unsupported by Microsoft (requiring a custom parallel task library from another article I wrote), then only look in the __cplusplus_winrt
areas as the programming pattern is slightly different. The callback to WinJS must not occur on the same thread which got the UriToStreamAsync
event or a STA (single threaded apartment) to STA error will be thrown and crash the application. The event callback is not currently wrapping the Uri in a special event args object so it needs to be accessed through the details property that WinJS will automatically provide for unknown event parameters.
The IDL for native C++ Wrappers.idl is as follows:
#include <sdkddkver.h>
#ifndef __cplusplus_winrt
namespace Wrappers
{
[object]
[uuid(94BEE8B5-6B7D-4670-A968-E2E3076FCFD2)]
[pointer_default(unique)]
[version(NTDDI_VERSION)]
interface IUriToStreamResolverWrapper : IInspectable
{
[eventadd] HRESULT UriToStreamAsyncCalled([in]
Windows.Foundation.EventHandler<iinspectable*>* handler,
[out][retval] EventRegistrationToken* cookie);
[eventremove] HRESULT UriToStreamAsyncCalled([in] EventRegistrationToken cookie);
HRESULT UriToStreamAsyncResolved([in] Windows.Foundation.Uri* uri,
[in] Windows.Storage.Streams.IInputStream* inputStream);
}
[activatable(NTDDI_VERSION)]
[threading(sta)]
[marshaling_behavior(agile)]
[version(NTDDI_VERSION)]
runtimeclass UriToStreamResolverWrapper
{
[default] interface IUriToStreamResolverWrapper;
}
}
#endif
Wrappers.cpp contains both native C++ and C++/CX using conditional compilation. Wrappers_h.h is the generated IDL header file with the interfaces. One might question the design pattern as the promises returned later from the WinJS functions are not used and instead a callback on the interface is used. In fact, more in spirit with the WinRT model would be to store the IAsyncOperation
's wrapping the Streams and waiting for their completion method. This would not require the extra method call, and would eliminate the 2 maps in exchange for an array. Yet the current method should work in all cases even if there is a very slight inefficiency.
#ifdef _WINRT_DLL
#include <map>
#ifndef __cplusplus_winrt
#include "Wrappers_h.h"
#include <windows.web.h>
#endif
namespace Wrappers {
#ifdef __cplusplus_winrt
[uuid("94BEE8B5-6B7D-4670-A968-E2E3076FCFD2")]
public interface class IUriToStreamResolverWrapper
{
virtual event Windows::Foundation::EventHandler
<Windows::Foundation::Uri^>^ UriToStreamAsyncCalled;
void UriToStreamResolved(Windows::Foundation::Uri^ uri,
Windows::Storage::Streams::IInputStream^ inputStream);
};
#endif
}
#ifdef __cplusplus_winrt
#define IUriToStreamResolverWrapper
TimeStamp::WebCamSecUtils::IUriToStreamResolverWrapper
DEFINE_GUID(IID_IUriToStreamResolverWrapper, 0x94bee8b5,
0x6b7d, 0x4670, 0xa9, 0x68, 0xe2, 0xe3, 0x7, 0x6f, 0xcf, 0xd2);
#define IUriToStreamResolverWrapperObj IUriToStreamResolverWrapper^
#else
#define IUriToStreamResolverWrapper ABI::TimeStamp::WebCamSecUtils::IUriToStreamResolverWrapper
#define IID_IUriToStreamResolverWrapper
ABI::TimeStamp::WebCamSecUtils::IID_IUriToStreamResolverWrapper
#define IUriToStreamResolverWrapperObj _ComPtr<IUriToStreamResolverWrapper>
#endif
namespace Wrappers {
#ifdef __cplusplus_winrt
public ref class UriToStreamResolverWrapper sealed :
public IUriToStreamResolverWrapper, Windows::Web::IUriToStreamResolver
#else
class UriToStreamResolverWrapper :
public Microsoft::WRL::RuntimeClass<
Microsoft::WRL::RuntimeClassFlags< Microsoft::WRL::RuntimeClassType::WinRt >,
IUriToStreamResolverWrapper, ABI::Windows::Web::IUriToStreamResolver>
#endif
{
#ifndef __cplusplus_winrt
InspectableClass
(RuntimeClass_TimeStamp_WebCamSecUtils_UriToStreamResolverWrapper, BaseTrust)
#endif
public:
#ifdef __cplusplus_winrt
virtual event Windows::Foundation::EventHandler
<Windows::Foundation::Uri^>^ UriToStreamAsyncCalled;
virtual void UriToStreamResolved(Windows::Foundation::Uri^ uri,
Windows::Storage::Streams::IInputStream^ inputStream)
{
std::wstring uristr;
UINT32 len;
PCWSTR rawbuf = WindowsGetStringRawBuffer((HSTRING)uri->RawUri, &len);
uristr.assign(rawbuf, rawbuf + len);
{
AutoLock lock(m_CritSec);
m_map.insert(std::pair<std::wstring,
Windows::Storage::Streams::IInputStream^>(uristr, inputStream));
m_SignalMap.at(uristr)->set();
}
}
#else
STDMETHOD(RuntimeClassInitialize)() { return S_OK; }
STDMETHODIMP add_UriToStreamAsyncCalled
(__FIEventHandler_1_IInspectable *handler, EventRegistrationToken *cookie)
{
return m_events.Add(Microsoft::WRL::Make<ContextEventWrapper
<__FIEventHandler_1_IInspectable>>(handler).Detach(), cookie);
}
STDMETHODIMP remove_UriToStreamAsyncCalled(EventRegistrationToken cookie)
{
return m_events.Remove(cookie);
}
HRESULT STDMETHODCALLTYPE UriToStreamAsyncResolved(
ABI::Windows::Foundation::IUriRuntimeClass *uri,
ABI::Windows::Storage::Streams::IInputStream *inputStream)
{
std::wstring uristr;
HSTRING hstr;
UINT32 len;
PCWSTR rawbuf;
if (SUCCEEDED(uri->get_RawUri(&hstr))) {
rawbuf = WindowsGetStringRawBuffer(hstr, &len);
uristr.assign(rawbuf, rawbuf + len);
WindowsDeleteString(hstr);
}
{
AutoLock lock(m_CritSec);
m_map.insert(std::pair<std::wstring, Microsoft::WRL::ComPtr
<ABI::Windows::Storage::Streams::IInputStream>>
(uristr, inputStream));
m_SignalMap.at(uristr)->set();
}
return S_OK;
}
#endif
#ifndef __cplusplus_winrt
virtual HRESULT STDMETHODCALLTYPE UriToStreamAsync(
__RPC__in_opt
ABI::Windows::Foundation::IUriRuntimeClass *uri,
__RPC__deref_out_opt
__FIAsyncOperation_1_Windows__CStorage__CStreams__CIInputStream
**operation)
{
if (!operation) return E_INVALIDARG;
Microsoft::WRL::ComPtr<ABI::Windows::Foundation::IUriRuntimeClass>
puri = uri;
AddRef();
*operation = Concurrency_winrt::create_async
<ABI::Windows::Storage::Streams::IInputStream*>
([=](ABI::Windows::Storage::Streams::IInputStream**
retVal) -> HRESULT {
std::wstring uristr;
HSTRING hstr;
UINT32 len;
PCWSTR rawbuf;
if (SUCCEEDED(uri->get_RawUri(&hstr))) {
rawbuf = WindowsGetStringRawBuffer(hstr, &len);
uristr.assign(rawbuf, rawbuf + len);
WindowsDeleteString(hstr);
}
Concurrency::event signal;
{
AutoLock lock(m_CritSec);
m_SignalMap.insert(std::pair<std::wstring,
concurrency::event*>(uristr, &signal));
}
m_events.InvokeAll<IInspectable*,
IInspectable*>((IUriToStreamResolverWrapper*)this,
puri.Get()); signal.wait();
{
AutoLock lock(m_CritSec);
*retVal = m_map.at(uristr).Detach();
m_SignalMap.erase(uristr);
m_map.erase(uristr);
}
Release();
END_CREATE_ASYNC(S_OK);
return S_OK;
}
#else
virtual Windows::Foundation::IAsyncOperation
<Windows::Storage::Streams::IInputStream^>^
UriToStreamAsync(Windows::Foundation::Uri^ uri)
{
UriToStreamResolverWrapper^ _this = this;
Platform::WeakReference wr(this);
return Concurrency::create_async([wr, uri]() ->
Windows::Storage::Streams::IInputStream^ {
UriToStreamResolverWrapper^ _this =
wr.Resolve<UriToStreamResolverWrapper>();
Windows::Storage::Streams::IInputStream^ inputStream;
std::wstring uristr;
UINT32 len;
PCWSTR rawbuf = WindowsGetStringRawBuffer
((HSTRING)uri->RawUri, &len);
uristr.assign(rawbuf, rawbuf + len);
Concurrency::event signal;
{
AutoLock lock(_this->m_CritSec);
_this->m_SignalMap.insert(std::pair<std::wstring,
concurrency::event*>(uristr, &signal));
}
_this->UriToStreamAsyncCalled(_this, uri);
signal.wait();
{
AutoLock lock(_this->m_CritSec);
inputStream = _this->m_map.at(uristr);
_this->m_SignalMap.erase(uristr);
_this->m_map.erase(uristr);
}
return inputStream;
});
}
#endif
private:
#ifdef __cplusplus_winrt
static Windows::Foundation::EventRegistrationToken UriToStreamAsyncCalledAdd
(IUriToStreamResolverWrapperObj iCallback, Windows::Foundation::EventHandler
<Windows::Foundation::Uri^>^ handler) {
return iCallback->UriToStreamAsyncCalled += handler;
}
static void UriToStreamAsyncCalledRemove
(IUriToStreamResolverWrapperObj iCallback,
Windows::Foundation::EventRegistrationToken token) {
iCallback->UriToStreamAsyncCalled -= token;
}
EventBinder<IUriToStreamResolverWrapper, IUriToStreamResolverWrapperObj,
Windows::Foundation::EventHandler<Windows::Foundation::Uri^>^,
&UriToStreamAsyncCalledAdd, &UriToStreamAsyncCalledRemove> m_EventBinder;
#else
EventBinder<IUriToStreamResolverWrapper, IUriToStreamResolverWrapperObj,
ABI::Windows::Foundation::__FIEventHandler_1_IInspectable_t*,
&IUriToStreamResolverWrapper::add_UriToStreamAsyncCalled,
&IUriToStreamResolverWrapper::remove_UriToStreamAsyncCalled> m_EventBinder;
#endif
#ifndef __cplusplus_winrt
Microsoft::WRL::EventSource<ContextEventWrapper
<__FIEventHandler_1_IInspectable>> m_events;
#endif
CritSec m_CritSec;
std::map<std::wstring, Concurrency::event*> m_SignalMap;
#ifndef __cplusplus_winrt
std::map<std::wstring, Microsoft::WRL::ComPtr
<ABI::Windows::Storage::Streams::IInputStream>> m_map;
#else
std::map<std::wstring, Windows::Storage::Streams::IInputStream^> m_map;
#endif
};
}
#endif
Now to use this from WinJS is relatively simple, yet note the special URL replacement function and how it is built using the GUID of the app and the key that was used. It shows how one can actually swap default.html with the live document, and remove all JavaScript to set up for example to make a screenshot before disposing the webview. The navigation is monitored as well for completion and errors.
var getUriToStreamAsync = function (streamResolver) {
return function (uri) {
var toHex = function(str) {
var tempstr = '';
for (var i = 0; i < str.length; i++) {
tempstr = tempstr + str.charCodeAt(i).toString(16);
}
return tempstr;
}
if (uri.detail[0].rawUri === "ms-local-stream://" +
Windows.ApplicationModel.Package.current.id.name.toLowerCase() +
"_" + toHex("default") + "/default.html" ||
uri.detail[0].rawUri.indexOf(".js", uri.detail[0].rawUri.length - 3) !== -1) {
var randomAccessStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
var dataWriter = new Windows.Storage.Streams.DataWriter(randomAccessStream);
dataWriter.unicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.utf8;
dataWriter.byteOrder = Windows.Storage.Streams.ByteOrder.littleEndian;
dataWriter.writeString(uri.detail[0].rawUri.indexOf
(".js", uri.detail[0].rawUri.length - 3) !== -1 ?
'' : document.documentElement.outerHTML);
return dataWriter.storeAsync().then(function () {
return dataWriter.flushAsync();
}).then(function () {
var stream = dataWriter.detachStream();
stream.seek(0);
dataWriter.close();
streamResolver.uriToStreamAsyncResolved(uri, stream);
});
} else {
return Windows.Storage.StorageFile.getFileFromApplicationUriAsync
(new Windows.Foundation.Uri(uri.detail[0].rawUri.replace
("ms-local-stream://" + Windows.ApplicationModel.Package.current.id.name.toLowerCase() +
"_" + toHex("default"),
"ms-appx://"))).then(function (file) {
return file.openReadAsync();
}).then(function (stream) {
streamResolver.uriToStreamAsyncResolved(uri, stream);
});
}
};
};
var _this = this, webViewControl = window.document.createElement
("x-ms-webview"), streamResolver =
new TimeStamp.WebCamSecUtils.UriToStreamResolverWrapper(),
uriGetter = this.getUriToStreamAsync(streamResolver);
webViewControl.width = width;
webViewControl.height = height;
webViewControl.settings.isJavaScriptEnabled = false;
return new WinJS.Promise(function (complete) {
var navComplete = function () {
webViewControl.removeEventListener
("MSWebViewNavigationCompleted", navComplete);
complete();
};
streamResolver.addEventListener("uritostreamasynccalled", uriGetter);
webViewControl.addEventListener("MSWebViewNavigationCompleted", navComplete);
webViewControl.addEventListener("MSWebViewScriptNotify", navComplete);
webViewControl.addEventListener("MSWebViewUnviewableContentIdentified", navComplete);
webViewControl.navigateToLocalStreamUri
(webViewControl.buildLocalStreamUri
("default", "/default.html"), streamResolver);
window.document.body.appendChild(webViewControl);
});
Points of Interest
The control shown is not implementing events on a singleton pattern so it is up to the JavaScript not to respond to the event callback more than one time. Perhaps there is a better delegate model to enforce this yet delegates are just glorified singleton events for the most part.
The object implemented for the callback interface is called in parallel, hence a map is needed to identify and sequence the JavaScript callbacks which must also use the uri as a key. There could be additional logic needed and the reason Microsoft perhaps did not offer it in WinJS was to keep things highly parallel without being synchronized on the UI thread. However WinJS will still quickly return promises and do most processing in the background so the performance should be negligibly different. It might be better to implement the interface purely in the library though if loading large amounts of files very frequently. It is the author's opinion that Microsoft should implement such a wrapper and provide it for the WinJS library to make this easier and possible given it has been around since Windows 8.1 and is supposedly supported.
History