Embedding Tableau Server Views in a C# Application

It is possible, and not even that difficult, to embed Tableau Server views into a C# based Windows application. However, the putting all the pieces together to make it work smoothly is pretty difficult — the documentation on the Internet that describes the HOW is all over the place and unless you really know what Tableau is doing, it is not obvious what pieces of the .Net framework are actually useful. Hopefully this post will put it all together enough for anyone interested to get started.

Preface: I’m not a C# programmer and my entire knowledge of this is centered around the WebBrowser part and how it interacts with Tableau Server

Update 2015-07-02: Adding to this and Part 2 with far more depth

You will need:

  • Tableau Server
  • A separate web server, with some facility for dynamic web pages. Apache + PHP, or IIS + ASP.Net for example.
  • Visual Studio

The Web Server

Why a separate web server? While you can always use the direct embed links to bring up a Tableau viz from Server, if you want it to integrate well:

  • JavaScript API needs a wrapper web page to initialize from
  • Trusted Authentication needs a web server to trust
  • The correct headers in the web page will force the embedded IE browser to render the right way

JavaScript api

The JavaScript API has to be started from a web page, no matter how basic it is. JS API is useful for all the control it gives such as sizing, toolbar options and so forth. Plus, it is also the only facility for interaction between the Tableau Viz and the application.

Authentication

If your authentication is entirely Active Directory with ‘Enable automatic logon’ (Set in the ‘Configure Tableau Server’ program) , you are in luck! You don’t even have to worry about the trusted authentication portion. Because the WebBrowser object in .Net is calling the local Internet Explorer on the computer, SSPI should work just fine.

For trusted auth, you don’t need anything incredibly complex from the dynamic web server. As long as it can receive the Ticket from the trusted auth request and substitute that in to the URL request, you’ll get smooth SSO in your application. You can POST data to the dynamic page (seen below in the C# code), for example the username, and if your Tableau Server is set up with SSL, the whole process is completely secure.

Forcing standards compliant rendering

By default, the WebBrowser object renders all its content using IE7 compatibility mode. If you want to see Tableau Server completely break, render everything in IE7 mode. Luckily, there is an easy way to force the use of the most modern rendering the system supports (IE10 or 11 hopefully).

The first lines of HTML in all the pages served up by your web server should be


<!doctype html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>

The doctype declaration and the meta tag together force IE to use it’s latest and greatest rendering engine, which is pretty good these days.

Updated: But this is not enough! For some reason, the WebBrowser part renders in IE10 Compatibility Mode, which while not being as terrible as IE7, still does things so incorrectly that it breaks the editing toolbar when using Web Edit. You will only see the Undo button and nothing else if you are in Compatibility mode. To ensure that the browser always does the right thing, you’ll have to add some code to your application that changes a few registry entries while the program is running.

Stack Overflow provides an answer, which I’ve modified here with only the necessary pieces (and also throwing out IE7 altogether since Tableau Server 9 can’t use it). I put the two classes in the main Form1 public class in the example file, then use the SetWebBrowserFeatures() method on initialization of the form.


public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
            SetWebBrowserFeatures();
            
        }

..........

        /*
         * This section handles forcing the WebBrowser into modern standards compliant mode
         * Without it, you'll get dropped into IE10 compatibility mode and not all the buttons in web edit show up
         * */

        // set WebBrowser features, more info: http://stackoverflow.com/a/18333982/1768303
        static void SetWebBrowserFeatures()
        {
            // don't change the registry if running in-proc inside Visual Studio
            if (LicenseManager.UsageMode != LicenseUsageMode.Runtime)
                return;

            var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);

            var featureControlRegKey = @"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\";

            Registry.SetValue(featureControlRegKey + "FEATURE_BROWSER_EMULATION",
                appName, GetBrowserEmulationMode(), RegistryValueKind.DWord);

            // enable the features which are "On" for the full Internet Explorer browser

            Registry.SetValue(featureControlRegKey + "FEATURE_ENABLE_CLIPCHILDREN_OPTIMIZATION",
                appName, 1, RegistryValueKind.DWord);

            Registry.SetValue(featureControlRegKey + "FEATURE_AJAX_CONNECTIONEVENTS",
                appName, 1, RegistryValueKind.DWord);

            Registry.SetValue(featureControlRegKey + "FEATURE_GPU_RENDERING",
                appName, 1, RegistryValueKind.DWord);

            Registry.SetValue(featureControlRegKey + "FEATURE_WEBOC_DOCUMENT_ZOOM",
                appName, 1, RegistryValueKind.DWord);

            Registry.SetValue(featureControlRegKey + "FEATURE_NINPUT_LEGACYMODE",
                appName, 0, RegistryValueKind.DWord);
        }

        static UInt32 GetBrowserEmulationMode()
        {
            int browserVersion = 0;
            using (var ieKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Internet Explorer",
                RegistryKeyPermissionCheck.ReadSubTree,
                System.Security.AccessControl.RegistryRights.QueryValues))
            {
                var version = ieKey.GetValue("svcVersion");
                if (null == version)
                {
                    version = ieKey.GetValue("Version");
                    if (null == version)
                        throw new ApplicationException("Microsoft Internet Explorer is required!");
                }
                int.TryParse(version.ToString().Split('.')[0], out browserVersion);
            }

            if (browserVersion < 8)
            {
                throw new ApplicationException("Unsupported version of Microsoft Internet Explorer!");
            }

            UInt32 mode = 11000; // Internet Explorer 11. Webpages containing standards-based !DOCTYPE directives are displayed in IE11 Standards mode. 

            switch (browserVersion)
            {
                case 7:
                    mode = 7000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE7 Standards mode.
                    break;
                case 8:
                    mode = 8000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE8 mode.
                    break;
                case 9:
                    mode = 9000; // Internet Explorer 9. Webpages containing standards-based !DOCTYPE directives are displayed in IE9 mode.                    
                    break;
                case 10:
                    mode = 10000; // Internet Explorer 10.
                    break;
            }

            return mode;
        }

The Tableau Server

Configure it to trust the web server, per all the trusted auth setup. That’s all you need to do.

THE APPLICATION

Initializing the Web Browser to the page

The crux of everything is the .Net WebBrowser control. Bring one into your page layout ( in this example it is webBrowser1 ).

As mentioned earlier, if your Tableau Server doesn’t have AD with automatic logon turned on, you’ll want to use HTTP POST to pass the username over to the dynamic page. You do this with the WebBrowser.Navigate method .


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Security.Permissions;

namespace Embedded_Tableau_Example
{
    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    [System.Runtime.InteropServices.ComVisibleAttribute(true)]
    public partial class Form1 : Form
    {
        int tab_count = 1;
        String username = ""; // Here you can get your username however you'd like
        public Form1()
        {
            InitializeComponent();
            
        }

        private void Form1_Load(object sender, EventArgs e)
        {

            string postData = String.Format("username={0}", username);
            byte[] Post = Encoding.UTF8.GetBytes(postData);
            string AdditionalHeaders = "Content-Type: application/x-www-form-urlencoded";

            var page_location = "http://{TableauServerLocation}/c_sharp_embed.php"; // HTTPS for security in final environment
            //webBrowser1.Navigate(page_location); // If using AD with automatic logon
            webBrowser1.Navigate(page_location, "_self", Post, AdditionalHeaders);
            webBrowser1.ObjectForScripting = this;

        }

        private void button1_Click(object sender, EventArgs e)
        {
            webBrowser1.Document.InvokeScript("pdfExport");
        }

        private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {

        }

        private void button2_Click(object sender, EventArgs e)
        {
            
            object[] args = { tab_count };

            webBrowser1.Document.InvokeScript("switchTab", args);
            tab_count++; 

        }

        public void Test(String message)
        {
            MessageBox.Show(message, "client code");
        }

        private void button3_Click(object sender, EventArgs e)
        {
            webBrowser1.Document.InvokeScript("returnJSONString");
        }

        public void retrieveJSON(String json)
        {
            MessageBox.Show(json, "Stringy JSON");
        }

    }
}

ExtendedWebBrowser control to reroute popups

Everything in the above section is true, but the standard WebBrowser control has a major flaw — you can cancel the NewWindow event, but you can’t get any information about it. Web Edit, See Underlying Data, and all of the Exports (PDF, PNG, Crosstab) by default open a new IE window, which is not desirable. The preferable action is capture the URLs of the new windows and reroute them back into our original WebBrowser control so that the actions all happen locally (particularly for the download methods). For some reason, the Windows.Forms version of the WebBrowser control does not expose all of the events that are actually happening. For example, NewWindow3 exists in the underlying control and provides the location for the new window being created via the bstrUrl string. How do we get to the NewWindow3 event though?

The solution provided by Mrojas at Art in Soft creates an ExtendedWebBrowser class that allows for capturing any of the events in the underlying controls. It’s also possible that this reference on Stack Overflow allows for the same thing with less code, but I was able to get the ExtendedWebBrowser code working and tested, so that’s my recommendation.

The entire project is available on GitHub. This is what I added to the Program.cs (I’ve added the necessary NewWindow3 event handler to the original from Art in Soft, with the DWebBrowserEvents2 from Pinvoke.net providing the COM interface code necessary)


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace Embedded_Tableau_Example
{
static class Program
{
/// <summary>
/// The main entry point for the application
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}

/*
* ExtendedWebBrowser class allows for additional events to be bubbled up out of the ActiveX control
* We need NewWindow3 so we can reroute any new windows that Tableau Server page tries to open up by default
*
*/
public class ExtendedWebBrowser : System.Windows.Forms.WebBrowser
{
AxHost.ConnectionPointCookie cookie;
WebBrowserExtendedEvents events;

protected override void CreateSink()
{
base.CreateSink();
events = new WebBrowserExtendedEvents(this);
cookie = new AxHost.ConnectionPointCookie(this.ActiveXInstance, events, typeof(DWebBrowserEvents2));
}

public object Application
{
get
{
IWebBrowser2 axWebBrowser = this.ActiveXInstance as IWebBrowser2;
if (axWebBrowser != null)
{
return axWebBrowser.Application;
}
else
return null;
}
}

protected override void DetachSink()
{
if (null != cookie)
{
cookie.Disconnect();
cookie = null;
}
base.DetachSink();
}

//This new event will fire for the NewWindow2
public event EventHandler<NewWindow2EventArgs> NewWindow2;

protected void OnNewWindow2(ref object ppDisp, ref bool cancel)
{
EventHandler<NewWindow2EventArgs> h = NewWindow2;
NewWindow2EventArgs args = new NewWindow2EventArgs(ref ppDisp, ref cancel);
if (null != h)
{
h(this, args);
}
//Pass the cancellation chosen back out to the events
//Pass the ppDisp chosen back out to the events
cancel = args.Cancel;
ppDisp = args.PPDisp;
}

public event EventHandler<NewWindow3EventArgs> NewWindow3;

protected void OnNewWindow3(ref object ppDisp, ref bool cancel, long dwFlags, string bstrUrlContext, string bstrUrl)
{
EventHandler<NewWindow3EventArgs> h = NewWindow3;
NewWindow3EventArgs args = new NewWindow3EventArgs(ref ppDisp, ref cancel, dwFlags, bstrUrlContext, bstrUrl );
if (null != h)
{
h(this, args);
}
//Pass the cancellation chosen back out to the events
//Pass the ppDisp chosen back out to the events
cancel = args.Cancel;
ppDisp = args.PPDisp;
bstrUrlContext = args.BStrURLContext;
bstrUrl = args.BStrURL;

}

//This new event will fire for the DocumentComplete
public event EventHandler<DocumentCompleteEventArgs> DocumentComplete;

protected void OnDocumentComplete(object ppDisp, object url)
{
EventHandler<DocumentCompleteEventArgs> h = DocumentComplete;
DocumentCompleteEventArgs args = new DocumentCompleteEventArgs(ppDisp, url);
if (null != h)
{
h(this, args);
}
//Pass the ppDisp chosen back out to the events
ppDisp = args.PPDisp;
//I think url is readonly
}

//This new event will fire for the DocumentComplete
public event EventHandler<CommandStateChangeEventArgs> CommandStateChange;

protected void OnCommandStateChange(long command, ref bool enable)
{
EventHandler<CommandStateChangeEventArgs> h = CommandStateChange;
CommandStateChangeEventArgs args = new CommandStateChangeEventArgs(command, ref enable);
if (null != h)
{
h(this, args);
}
}

//This class will capture events from the WebBrowser
public class WebBrowserExtendedEvents : System.Runtime.InteropServices.StandardOleMarshalObject, DWebBrowserEvents2
{
ExtendedWebBrowser _Browser;
public WebBrowserExtendedEvents(ExtendedWebBrowser browser)
{ _Browser = browser; }

//Implement whichever events you wish
public void NewWindow2(ref object pDisp, ref bool cancel)
{
_Browser.OnNewWindow2(ref pDisp, ref cancel);
}

public void NewWindow3(ref object pDisp, ref bool cancel, uint dwFlags, string bstrUrlContext, string bstrUrl)
{
_Browser.OnNewWindow3(ref pDisp, ref cancel, dwFlags, bstrUrlContext, bstrUrl);
}

//Implement whichever events you wish
public void DocumentComplete(object pDisp, ref object url)
{
_Browser.OnDocumentComplete(pDisp, url);
}

//Implement whichever events you wish
public void CommandStateChange(long command, bool enable)
{
_Browser.OnCommandStateChange(command, ref enable);
}

}
[ComImport, Guid("34A715A0-6587-11D0-924A-0020AFC7AC4D"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch), TypeLibType(TypeLibTypeFlags.FHidden)]
public interface DWebBrowserEvents2
{
[DispId(0x69)]
void CommandStateChange([In] long command, [In] bool enable);
[DispId(0x103)]
void DocumentComplete([In, MarshalAs(UnmanagedType.IDispatch)] object pDisp, [In] ref object URL);
[DispId(0xfb)]
void NewWindow2([In, Out, MarshalAs(UnmanagedType.IDispatch)] ref object pDisp, [In, Out] ref bool cancel);
[DispId(0x111)]
void NewWindow3([In, Out, MarshalAs(UnmanagedType.IDispatch)] ref object pDisp, [In, Out, MarshalAs(UnmanagedType.VariantBool)] ref bool Cancel, uint dwFlags, [MarshalAs(UnmanagedType.BStr)] string bstrUrlContext, [MarshalAs(UnmanagedType.BStr)] string bstrUrl);
}

[ComImport, Guid("D30C1661-CDAF-11d0-8A3E-00C04FC9E26E"), TypeLibType(TypeLibTypeFlags.FOleAutomation | TypeLibTypeFlags.FDual | TypeLibTypeFlags.FHidden)]
public interface IWebBrowser2
{
[DispId(100)]
void GoBack();
[DispId(0x65)]
void GoForward();
[DispId(0x66)]
void GoHome();
[DispId(0x67)]
void GoSearch();
[DispId(0x68)]
void Navigate([In] string Url, [In] ref object flags, [In] ref object targetFrameName, [In] ref object postData, [In] ref object headers);
[DispId(-550)]
void Refresh();
[DispId(0x69)]
void Refresh2([In] ref object level);
[DispId(0x6a)]
void Stop();
[DispId(200)]
object Application { [return: MarshalAs(UnmanagedType.IDispatch)] get; }
[DispId(0xc9)]
object Parent { [return: MarshalAs(UnmanagedType.IDispatch)] get; }
[DispId(0xca)]
object Container { [return: MarshalAs(UnmanagedType.IDispatch)] get; }
[DispId(0xcb)]
object Document { [return: MarshalAs(UnmanagedType.IDispatch)] get; }
[DispId(0xcc)]
bool TopLevelContainer { get; }
[DispId(0xcd)]
string Type { get; }
[DispId(0xce)]
int Left { get; set; }
[DispId(0xcf)]
int Top { get; set; }
[DispId(0xd0)]
int Width { get; set; }
[DispId(0xd1)]
int Height { get; set; }
[DispId(210)]
string LocationName { get; }
[DispId(0xd3)]
string LocationURL { get; }
[DispId(0xd4)]
bool Busy { get; }
[DispId(300)]
void Quit();
[DispId(0x12d)]
void ClientToWindow(out int pcx, out int pcy);
[DispId(0x12e)]
void PutProperty([In] string property, [In] object vtValue);
[DispId(0x12f)]
object GetProperty([In] string property);
[DispId(0)]
string Name { get; }
[DispId(-515)]
int HWND { get; }
[DispId(400)]
string FullName { get; }
[DispId(0x191)]
string Path { get; }
[DispId(0x192)]
bool Visible { get; set; }
[DispId(0x193)]
bool StatusBar { get; set; }
[DispId(0x194)]
string StatusText { get; set; }
[DispId(0x195)]
int ToolBar { get; set; }
[DispId(0x196)]
bool MenuBar { get; set; }
[DispId(0x197)]
bool FullScreen { get; set; }
[DispId(500)]
void Navigate2([In] ref object URL, [In] ref object flags, [In] ref object targetFrameName, [In] ref object postData, [In] ref object headers);
[DispId(0x1f7)]
void ShowBrowserBar([In] ref object pvaClsid, [In] ref object pvarShow, [In] ref object pvarSize);
[DispId(-525)]
WebBrowserReadyState ReadyState { get; }
[DispId(550)]
bool Offline { get; set; }
[DispId(0x227)]
bool Silent { get; set; }
[DispId(0x228)]
bool RegisterAsBrowser { get; set; }
[DispId(0x229)]
bool RegisterAsDropTarget { get; set; }
[DispId(0x22a)]
bool TheaterMode { get; set; }
[DispId(0x22b)]
bool AddressBar { get; set; }
[DispId(0x22c)]
bool Resizable { get; set; }
}

}

public class NewWindow2EventArgs : CancelEventArgs
{
object ppDisp;

public object PPDisp
{
get { return ppDisp; }
set { ppDisp = value; }
}

public NewWindow2EventArgs(ref object ppDisp, ref bool cancel)
: base()
{
this.ppDisp = ppDisp;
this.Cancel = cancel;
}
}

public class NewWindow3EventArgs : CancelEventArgs
{
object ppDisp;
public string BStrURLContext;
public string BStrURL;

public object PPDisp
{
get { return ppDisp; }
set { ppDisp = value; }

}

public NewWindow3EventArgs(ref object ppDisp, ref bool cancel, long dwFlags, string bstrUrlContext, string bstrUrl)
: base()
{
this.ppDisp = ppDisp;
this.Cancel = cancel;
this.BStrURLContext = bstrUrlContext;
this.BStrURL = bstrUrl;
}
}

public class DocumentCompleteEventArgs : EventArgs
{
private object ppDisp;
private object url;

public object PPDisp
{
get { return ppDisp; }
set { ppDisp = value; }
}

public object Url
{
get { return url; }
set { url = value; }
}

public DocumentCompleteEventArgs(object ppDisp, object url)
{
this.ppDisp = ppDisp;
this.url = url;
}
}

public class CommandStateChangeEventArgs : EventArgs
{
private long command;
private bool enable;
public CommandStateChangeEventArgs(long command, ref bool enable)
{
this.command = command;
this.enable = enable;
}

public long Command
{
get { return command; }
set { command = value; }
}
}
}

The essential thing is that when NewWindow3 event fires (any time a link tries to open a new window in Internet Explorer), the NewWindow3EventArgs has a BStrURL property telling you the full URL that the new window would have loaded. BStrURLContext tells you the location of the window that generated the popup, but that’s not new information that we couldn’t get another way. It also includes the same Cancel property from the original NewWindow event, which we’ll use in the same way to stop the popup.

What does it take to make it work? Well, first we need to add an ExtendedWebBrowser to our layout. I’m such a novice at Visual Studio that I went ahead and just modified the Form1.Designer.cs by hand, but this works


namespace Embedded_Tableau_Example
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.button3 = new System.Windows.Forms.Button();
this.button4 = new System.Windows.Forms.Button();
this.webBrowser1 = new Embedded_Tableau_Example.ExtendedWebBrowser();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(0, 12);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(92, 23);
this.button1.TabIndex = 1;
this.button1.Text = "Export PDF";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// button2
//
this.button2.Location = new System.Drawing.Point(99, 12);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(75, 23);
this.button2.TabIndex = 2;
this.button2.Text = "Next Tab";
this.button2.UseVisualStyleBackColor = true;
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// button3
//
this.button3.Location = new System.Drawing.Point(181, 13);
this.button3.Name = "button3";
this.button3.Size = new System.Drawing.Size(127, 23);
this.button3.TabIndex = 3;
this.button3.Text = "Object Return?";
this.button3.UseVisualStyleBackColor = true;
this.button3.Click += new System.EventHandler(this.button3_Click);
//
// button4
//
this.button4.Location = new System.Drawing.Point(315, 11);
this.button4.Name = "button4";
this.button4.Size = new System.Drawing.Size(75, 23);
this.button4.TabIndex = 4;
this.button4.Text = "Edit";
this.button4.UseVisualStyleBackColor = true;
this.button4.Click += new System.EventHandler(this.button4_Click);
//
// webBrowser1
//
this.webBrowser1.Location = new System.Drawing.Point(0, 41);
this.webBrowser1.MinimumSize = new System.Drawing.Size(20, 20);
this.webBrowser1.Name = "webBrowser1";
this.webBrowser1.Size = new System.Drawing.Size(958, 588);
this.webBrowser1.TabIndex = 0;
this.webBrowser1.NewWindow3 += new System.EventHandler<Embedded_Tableau_Example.NewWindow3EventArgs>(this.webBrowser1_NewWindow3);
this.webBrowser1.DocumentCompleted += new System.Windows.Forms.WebBrowserDocumentCompletedEventHandler(this.webBrowser1_DocumentCompleted);
this.webBrowser1.Navigating += new System.Windows.Forms.WebBrowserNavigatingEventHandler(this.webBrowser1_Navigating);
this.webBrowser1.NewWindow += new System.ComponentModel.CancelEventHandler(this.webBrowser1_NewWindow);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(970, 641);
this.Controls.Add(this.button4);
this.Controls.Add(this.button3);
this.Controls.Add(this.button2);
this.Controls.Add(this.button1);
this.Controls.Add(this.webBrowser1);
this.Name = "Form1";
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);

}

#endregion

private Embedded_Tableau_Example.ExtendedWebBrowser webBrowser1;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.Button button3;
private System.Windows.Forms.Button button4;
}
}

For how we handle Web Edit and the rest of the integration, see Part 2

Communicating from Tableau to The Application

So far we’ve loaded the page. But what if you want interaction between your the Tableau report and your application, or vice-versa?

The WebBrowser control can be set so that public methods can be called from the JavaScript in the browser out to application, via the WebBrowser.ObjectForScripting property . When the object is set to this, or any other object, that C# object can be referenced in JavaScript via window.external . For example, if you have a method called ‘receiveJS’, you can do


window.external.receiveJS('{obj : value}');

and this will pass the value to the object you defined. Per the documentation, you have to set the following two attributes on whichever class is going to be the ObjectForScripting


[PermissionSet(SecurityAction.Demand, Name="FullTrust")]
[System.Runtime.InteropServices.ComVisibleAttribute(true)]

It seems like all data is passed across as a string, which you can then deserialize and use however you’d like. If you are passing around enough data, it might make sense to send it all over as JSON objects and figure out how to handle it. For that task, of which I have no experience, there are the Microsoft defined JSON deserializers, and also lots of mentions of JSON.Net.

For capturing interactivity in the Tableau viz, you’ll most likely be attaching methods to the Event Handlers on the Viz class , then calling window.external once you’ve retrieved the data.

Communicating from the Application to the VIZ

You can use the WebBrowser.InvokeScript method to launch any function available in the window object of the DOM, even passing over the values for the parameters. You can set up functions to launch any of the UI functions on the Viz class, for example switching tabs or refreshing the viz. In the example code, there is one method that uses the argument object and one that does not.

 

Check out Part 2 for even more on integrating into an application.

Advertisements

2 comments

  1. Hello Bryant, first of all I would like to say, thank you for this post. We are actually looking to implement this at my job. Currently We built a reporting portal where our users can access the dashboards. Our reporting portal is designed to show dashboards to external users who are not logged into the company network or via VPN, but do have an Active Directory User ID on our network
    To accomplish this we currently have a dot-net web interface (which will be changed to SharePoint-2013 in near future).

    This is the procedure to be followed when any User accesses the dashboard portal.

    1. The User will access web portal from anywhere.

    2. The User will enter his AD login and be shown a dashboard reporting portal web page with dashboard images, each carrying a particular dashboard link.

    3. When the User clicks on a particular dashboard image it will pass the User credentials along with the URL of the dashboard chosen and Tableau needs to identify that particular User and authorize the User credentials on the Tableau server (without asking for the User credentials again) and show the User the specified dashboard. We have implemented Kerberos on our Tableau server to access an SSAS Cube data source and show the appropriate data for that particular User.

    So far we need to set up a trusted authentication on our server. Do you have any others ideas or recommendations? Additionally do you have an email I can contact you at, I will greatly appreciate your help.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s