04 September 2013 Browser, HowTo, IE, Metro, ObjectForScripting Robert Muehsig

Aus einer Desktop-Anwendung eine Web-Applikation aufzurufen ist trivial und es gibt je nach Art des Aufrufs unterschiedliche Wege. Man kann einen HTTP Request erzeugen, oder man verweisst einfach mit einem Link auf die Seite und der Browser geht auf oder man hat einen “embedded” Browser, der die Seite direkt im “App-Host” anzeigt. Recht einfach, aber wie könnte eine Web-Anwendung eine Desktop-Anwendung aufrufen?

Vom Web zur Desktop-Anwendung über URIs

Die erste Variante wären “Custom-URIs”, welche einfach als Link dargestellt werden. Bekanntestes Beispiel dürfte der “mailto” Link sein. Aber auch Skype, iTunes, Visual Studio, Spotify oder GitHub haben ähnliches gemacht. Wie das gemacht wird habe ich bereits hier beschrieben.

Vorteil: Lockere Bindung zwischen Web- und Desktop.
Nachteil: Die Interaktion geht vom “Web” aus und von der Funktionalität ist es wie ein Deep-Link in eine Desktop-Anwendung.

Weitere Informationen dazu in meinem Blogpost bzw. gab es bei der User Group Dresden ebenfalls eine Präsentation dazu.

Vom Web zur Desktop-Anwendung über window.external.notify

Auf diese Variante bin ich erst vor kurzem gestossen und war ein wenig begeistert. Grundgedanke ist, dass eine Desktop-Anwendung eine Web-Applikation über einen “embedded” WebBrowser anzeigen kann. Dem WebBrowser kann man ein “ObjectForScripting” mitgeben. Die angezeigte Webseite kann dann über den Javascript Aufruf von “window.external.notify(…)” das “ObjectForScripting” im Host aufrufen.

Ok – ganz langsam:

Dies ist die Web-App die wir in unserer WPF App “embedden” wollen:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>SamplePage for notify</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
</head>
<body>
    <h1>Foobar!</h1>
    <button onclick="foobar()">Click Me!</button>
    <script type="text/javascript">

        function foobar() {
            if (window.external == 'WebBrowserNotify.ScriptingContext') {
                window.external.notify("Hello World!");
            }
        }

    </script>
</body>
</html>

Der Aufbau ist simpel – drückt man einen Button soll die “notify”-Funktion mit “Hello World!” aufgerufen werden. Durch den Check auf den “WebBrowserNotify.ScriptingContext” bekomme ich in Chrome und IE keine Fehlermeldung wenn ich die Seite so aufrufe.

Die WPF-Applikation ist auch sehr simpel gemacht und besteht nur aus zwei Bereichen:

image

Der erste Kasten ist einfach nur das WebBrowser Control und im unteren Bereich ist ein TextBlock in dem ich später die Ergebnisse anzeigen möchte.

Im Xaml:

<Window x:Class="WebBrowserNotify.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <WebBrowser Height="200" x:Name="Host" />
        <TextBlock x:Name="Result" />
    </StackPanel>
</Window>

Code:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var scripting = new ScriptingContext();
            scripting.NotifyInvoked += (sender, args) =>
                                           {
                                               this.Result.Text += args.Result;
                                           };

            this.Host.ObjectForScripting = scripting;
            this.Host.Navigate("http://localhost:40414/Index.html");
        }
    }

    [ComVisible(true)]
    public class ScriptingContext
    {
        public delegate void NotifyInvokedEventHandler(object sender, NotifyInvokedEventArgs e);

        public event NotifyInvokedEventHandler NotifyInvoked;

        public void notify(string result)
        {
            if (NotifyInvoked != null)
            {
                var args = new NotifyInvokedEventArgs { Result = result };
                NotifyInvoked(this, args);
            }
        }
    }

    public class NotifyInvokedEventArgs : EventArgs
    {
        public string Result { get; set; }

    }

Erklärung:

Der “Host” ist ein WPF WebBrowser Objekt. Auf WinForms Seite gibt es sogar noch ein “mächtigeres” Control. Diesem geben wir ein “ObjectForScripting” mit. Dies muss “ComVisible” sein und hat per Konvention eine Methode “notify”. Das ist genau die Methode die aus dem Javascript aufgerufen wird. Im ScriptingContext habe ich nur noch ein Event definiert und darauf registriert sich der Code. Am Ende wird bei jedem Klick in der WebApp das Ergebnis in der WPF-Anwendung ausgegeben.

Hier gibt es noch eine Erklärung von WPF Team. Kleine Erinnerung: In der Web-Anwendung habe ich external == “WebBrowserNotify.ScriptingContext” abgefragt – dies ist einfach nur der Namespace + der Klassenname des “ObjectForScripting”. Man kann diese Abfrage aber auch anders gestalten.

Wichtig: Wo die WebApp läuft ist egal.

Die Web-Anwendung kann irgendwo laufen – lokal, im Intranet oder irgendwo anders. Im Grunde ist es ein normaler WebBrowser welcher in der Anwendung genutzt wird, nur das noch zusätzlich ein Objekt mitgegeben wird, welches aus dem Javascript aufgerufen werden kann.

Was macht man damit?

Ich kann mir gut vorstellen das iTunes im Grunde sowas macht. Soweit ich dies mal beobachtet hab ist der iTunes Client auch nur ein “Browser”. Um bestimmte Daten von der Web-Anwendung an den Host weiterzugeben wäre so eine Variante vorstellbar.

Auch Authentifizierungs-Dialog können so umgesetzt werden: Ein Fat-Client authentifiziert sich damit gegen eine Web-Anwendung und nach der Authentifizierung wird die “notify” Methode im Fat-Client aufgerufen. Dies ist die Mechanik die sich auch Microsoft mit der Azure Authentication Library zunutze macht. Ein iOS Entwickler hat dieses Vorgehen sogar im UIWebView zum Laufen gebracht.

window.external.notify – WinRT & Windows Phone Support

Auf der Windows-Desktop Welt kann man dem ObjectForScripting beliebige Methoden mitgeben. Auf WinRT und Windows Phone gelang es mir aber nur mit der “notify” Methode – andere Methoden waren nicht unterstützt. Aber man kann beliebige Daten über diese Methode tunneln (z.B. als JSON). Wie oben bereits verlinkt geht es wohl über Tricks auch mit iOS.

Fazit

Über solch ein Hosting einer WebApp innerhalb der eigenen Anwendung kann man recht interessante Szenarios abdecken.

Den kompletten Sample-Code gibt es wie immer auf GitHub.


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!