This project has moved. For the latest updates, please go here.
COM has become a vital part of DesktopGap thanks to the chosen technology: Microsoft’s .NET platform (v 4.5) with C# as a language and Internet Explorer’s Trident engine (i.e. the C# WebBrowser control).

This has been done for the following reasons:
  • Prior knowledge
  • Dominance of Windows in business environments
  • Ease of implementation due to .NET’s integration into Windows
  • Interoperability between .NET and Internet Explorer
  • Availability of Trident
  • Automated updates of Trident via Windows Update
  • Minimize dependencies to ease deployment and remain compatible to Mono

Using (and customizing) this control however also means using COM since IE is largely COM-based (http://msdn.microsoft.com/en-us/library/aa741312.aspx). But now for some specifics:

WebBrowser Customization

The core part of the customized WebBrowser control consists of several classes:
com-webbrowser.png

As shown, the browser's base class is abstract and derived from WebBrowser in order to provide the basic infrastructure to customize two important aspects: DocumentHostUiHandler and WebBrowserEvents. These two are responsible for providing UI-centric and web-centric functionality using .NET's COM interop (defined in the interfaces). Again, the abstract base classes implement defaults for their implementations to override in order to improve readability.

Furthermore there is a MSDN article about customizing the WebBrowser control in C++.

DocHostUIHandler

IDocHostUIHandler offers several methods, as an example for their definition here two important ones:
[return: MarshalAs (UnmanagedType.I4)]
[PreserveSig]
int GetDropTarget (
    [In] [MarshalAs (UnmanagedType.Interface)] IDropTarget pDropTarget,
    [Out] [MarshalAs (UnmanagedType.Interface)] out IDropTarget ppDropTarget);

[return: MarshalAs (UnmanagedType.I4)]
[PreserveSig]
int GetExternal ([Out] [MarshalAs (UnmanagedType.IDispatch)] out object ppDispatch);


DocHostUIHandler provides a reasonable default implementation to restore functionality:
public virtual int GetDropTarget (IDropTarget pDropTarget, out IDropTarget ppDropTarget)
{
  ppDropTarget = null;
  return HResult.E_NOTIMPL;
}

public virtual int GetExternal (out object ppDispatch)
{
  if (_browser.ObjectForScripting != null)
  {
    ppDispatch = _browser.ObjectForScripting;
    return HResult.S_OK;
  }
  ppDispatch = null;
  return HResult.S_FALSE;
}


As obvious in the snippets, the primary reason for including this customization is to provide a scripting interface to JavaScript and enable a drag and drop implementation. In order to link the DocHostUIHandler with a specific WebBrowser object, the root document's UI handler has to be replaced, but the original DocHostUIhandler has to be deactivated first by setting the "RegisterAsDropTarget" property to false.
protected void InstallCustomUIHandler (IDocHostUIHandler docHostUiHandler)
{
  ArgumentUtility.CheckNotNull ("docHostUiHandler", docHostUiHandler);
  DocumentHostUiHandler = docHostUiHandler;
  AxIWebBrowser2.RegisterAsDropTarget = false;
  var customDoc = (ICustomDoc) AxIWebBrowser2.Document;
  if (customDoc != null)
    customDoc.SetUIHandler (docHostUiHandler);
}


!!DWebBrowserEvents2

This interface holds event handlers for common web events like BeforeNavigate2, NewWindow3, etc.
[DispId (250)]
void BeforeNavigate2 (
    [MarshalAs (UnmanagedType.IDispatch)] object pDisp,
    [In] ref object URL,
    [In] ref object Flags,
    [In] ref object TargetFrameName,
    [In] ref object PostData,
    [In] ref object Headers,
    [In] [Out] [MarshalAs (UnmanagedType.VariantBool)] ref bool Cancel);

[DispId (0x111)]
void NewWindow3 (
    [In] [Out] [MarshalAs (UnmanagedType.IDispatch)] ref object ppDisp,
    [In] [Out] [MarshalAs (UnmanagedType.VariantBool)] ref bool Cancel,
    uint dwFlags,
    [MarshalAs (UnmanagedType.BStr)] string bstrUrlContext,
    [MarshalAs (UnmanagedType.BStr)] string bstrUrl);


Again, the corresponding base class holds the default implementation (in this case NOOPs), and ultimately, it should forward the method calls etc. to an appropriate location (in this case the browser instance itself):
    public override void NewWindow3 (ref object ppDisp, ref bool Cancel, uint dwFlags, string bstrUrlContext, string bstrUrl)
    {
      var ppDispOriginal = ppDisp;
      var eventArgs = new WindowOpenEventArgs (BrowserWindowTarget.PopUp, Cancel, new Uri (bstrUrl, UriKind.Absolute));

      _browserControl.OnNewWindow (eventArgs);

      if (eventArgs.TargetView != null)
        ppDisp = ((TridentWebBrowserBase) eventArgs.TargetView).Application ?? ppDispOriginal;

      Cancel = eventArgs.Cancel;
    }


To associate this implementation with the current browser instance, the following method is called by MSHTML. Then
[PermissionSet (SecurityAction.LinkDemand, Name = "FullTrust")]
protected override void CreateSink ()
{
  // Make sure to call the base class or the normal events won't fire
  base.CreateSink();
  if (BrowserEvents == null)
    return;
  _cookie = new AxHost.ConnectionPointCookie (ActiveXInstance, BrowserEvents, typeof (DWebBrowserEvents2));
}


Afterwards the connection is established and the events are propagated to the provided implementation (BrowserEvents in this case).

Resource Filtering

In order to restrict resources from being retrieved, the WebBrowser control offers no suitable methods, which is why the protocol itself has been wrapped to offer filtering (and wrap the default functionality of course). As of this writing, these protocols have been linked to a switch in the configuration, since even a plain wrapping causes many side effects which cannot be resolved ...

Asynchronous Pluggable Protocols

As described in this MSDN article, these components are intended for handling custom protocols. However with a little bit of research, the (hardcoded) GUIDs of the original HTTP and HTTPS protocols can be found and thus imported via COM. Similar to what the Passthrough APP does (can be found here), the required interfaces are implemented, passing the calls on to the HTTP(S) implementation if required (i.e. when the URL is not to be filtered).

Unfortunately, in C# this is not as easy as it seems. Firstly, the HTTP protocol does not like concurrency and requires a STA, whereas MTA is actually used with C# and COM and a registry key is required to avoid this behavior. This solution however would require to expose the assembly to COM which is not intended, since it should only be loaded temporarily. Hence, a workaround has to be found: Dispatchers. C#'s visual controls offer a dispatcher to run stuff on the UI thread, which guarantees that only the same thread executes methods on the original HTTP protocol handler.
Moreover it seems to be required that all protocol handler instances seem to require the same thread to run properly, hence the same dispatcher object has to be passed to multiple handler instances. Luckily a factory is required anyway, thus providing an easy solution to that.
[ComVisible (true)]
public class FilteredHttpProtocolFactory : IProtocolFactory, IDisposable
{
  private readonly IUrlFilter _urlFilter;
  private readonly Control _ctrl;
  private int _lockCount;
  private readonly Dispatcher _dispatcher;

  public FilteredHttpProtocolFactory (IUrlFilter urlFilter)
  {
    ArgumentUtility.CheckNotNull ("urlFilter", urlFilter);

    _urlFilter = urlFilter;
    _ctrl = new Control();
    _dispatcher = _ctrl.Dispatcher;
  }
  // ...
  public void CreateInstance (object pUnkOuter, Guid riid, out object ppvObject)
  {
    ppvObject = new FilteredHttpProtocol (_dispatcher, _urlFilter);
  }


IProtocolFactory implements IClassFactory which is the COM interface necessary for protocol handler factories. Afterwards, the methods are called according to this workflow: http://msdn.microsoft.com/en-us/library/aa767916(v=vs.85).aspx, beginning with Start.
  [ComImport]
  [ComVisible (false)]
  [InterfaceType (ComInterfaceType.InterfaceIsIUnknown)]
  [Guid ("00000001-0000-0000-C000-000000000046")]
  public interface IClassFactory
  {
    void CreateInstance (
        [MarshalAs (UnmanagedType.IUnknown)] object pUnkOuter,
        [MarshalAs (UnmanagedType.LPStruct)] Guid riid,
        [MarshalAs (UnmanagedType.IUnknown)] out object ppvObject);

    void LockServer ([MarshalAs (UnmanagedType.Bool)] bool fLock);
  }


So, everytime a request is made, the registered factory is asked to provide an instance of IInternetProtocol to handle a request.
[InterfaceType (ComInterfaceType.InterfaceIsIUnknown)]
[Guid ("79EAC9E4-BAF9-11CE-8C82-00AA004BA90B")]
public interface IInternetProtocol
{
  void Start (
      [MarshalAs (UnmanagedType.LPWStr)] string szURL,
      IInternetProtocolSink Sink,
      IInternetBindInfo pOIBindInfo,
      UInt32 grfPI,
      UInt32 dwReserved);
  void Continue (ref _tagPROTOCOLDATA pProtocolData);
  void Abort (Int32 hrReason, UInt32 dwOptions);
  void Terminate (UInt32 dwOptions);
  void Suspend ();
  void Resume ();

  [PreserveSig]
  UInt32 Read (IntPtr pv, UInt32 cb, out UInt32 pcbRead);
  void Seek (_LARGE_INTEGER dlibMove, UInt32 dwOrigin, out _ULARGE_INTEGER plibNewPosition);
  void LockRequest (UInt32 dwOptions);
  void UnlockRequest ();
}


Also the creation of the original HTTP protocol handler has to be done in the dispatcher's thread. Furthermore, the Start() method signals the beginning of a download and therefore - if a resource is considered illegal - the filter should be applied here and report that the resource could not be found so unhindered display of the page is still possible.
 [ComVisible (true)]
 [Guid ("E8253D6B-1AEE-4B5A-B7F9-F37D9C76C5FB")]
 public class FilteredHttpProtocol : IInternetProtocol, IInternetProtocolRoot, IDisposable
 {
   private IInternetProtocol _wrapped;
   private readonly Dispatcher _dispatcher;
   private readonly IUrlFilter _urlFilter;
   private string _currentUrl;
   private bool _isAllowed;

   public FilteredHttpProtocol (Dispatcher dispatcher, IUrlFilter urlFilter)
   {
     ArgumentUtility.CheckNotNull ("dispatcher", dispatcher);
     ArgumentUtility.CheckNotNull ("urlFilter", urlFilter);
     _urlFilter = urlFilter;
     _dispatcher = dispatcher;
     _dispatcher.Invoke (
         () =>
         {
           var originalHttpHandler = new HttpProtocol();
           _wrapped = (IInternetProtocol) originalHttpHandler;
         });
   }
   public void Start (string szURL, IInternetProtocolSink Sink, IInternetBindInfo pOIBindInfo, uint grfPI, uint dwReserved)
   {
     // How to do more complex stuff: http://www.codeproject.com/Articles/6120/A-Simple-protocol-to-view-aspx-pages-without-IIS-i
     _currentUrl = szURL;
     _isAllowed = _urlFilter.IsAllowed (szURL);
     if (!_isAllowed)
     {
       _dispatcher.Invoke (
           () => Sink.ReportResult (HResult.INET_E_RESOURCE_NOT_FOUND, (uint) HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToString()));
       return;
     }
     _dispatcher.Invoke (
         () => _wrapped.Start (szURL, Sink, pOIBindInfo, grfPI, dwReserved),
         new TimeSpan (0, 0, 0, 30));
   }
  // ...
  public uint Read (IntPtr pv, uint cb, out uint pcbRead)
  {
    uint result = 0;
    uint _pcbRead = 0;
    if (_isAllowed)
      _dispatcher.Invoke (() => result = _wrapped.Read (pv, cb, out _pcbRead));
    pcbRead = _pcbRead;
    return result;
  }
}

Issues

The main issues are rendering problems, which also sometimes lead to crashes (upon debugging often the Read() or even Start() methods of the wrapped object are waiting for a lock - which, since it is executed on the GUI thread, freezes the application), as well as including external objects (e.g. ActiveX) also unpredictably crashes for similar reasons.

These problems are yet to be resolved, but since there is little to no documentation for C# (with C++ some things are just a lot easier ;) ) solutions are very hard to come by.

What has been tried:
  • Avoid using the GUI thread as dispatcher thread: Leads to COM-related exceptions.
  • Use a timeout on the dispatcher call: Seems not to work.
  • Double-check interfaces and parameters: In C++ the Start() method seems to have a return value (a HRESULT), however in C# adding one will crash everything.
  • Use locking instead of threads: MTA execution will still lead to exceptions.

Alternatives

As an alternative HTTP(S) could be re-implemented as a whole (namespace: DesktopGap.Clients.Windows.Protocol.Wrapper.Experimental) using C# web requests and other things. However, this did not yield the expected results, which is suspected to be caused by a "wrong" order of method calls or similar confusions. This is also due to the lack of proper documentation, and the examples found on the internet have been helpful, but often too simple.
Another alternative would be using a locally running proxy where every request is redirected to (since proxies can only be set system-wide - especially for IE instances), which is also an experiment in this namespace.

Last edited Jun 28, 2013 at 10:30 AM by clma, version 2

Comments

No comments yet.