Custom Visualizers

A lot of people have asked for the ability to write custom visualizers for LINQPad. There are plenty of obvious uses:

  • Displaying values that change in real time
  • Showing charts and graphs
  • Interacting with data after the query finishes
  • Implementing an 'Open Table for Editing' feature for databases
If you just want to change the properties displayed by the Dump method, you can do so via the ToDump hook. Click here for details.

Writing a visualizer is easy to do: simply write an extension method that accepts the kind of object that you want to visualize, and in that method, .Dump() a WPF or Windows Forms control that displays what you want! LINQPad will display your UI control in an output panel and users can interact with it as they would normally.

And if you write your extension method in My Extensions, you'll be able to call it from any query.

PanelManager

Calling .Dump() on a WPF or Windows Forms control is a easy shortcut for displaying custom UI content in LINQPad. However, you can get more control over the process by calling the following static methods on LINQPad's PanelManager class:

public static OutputPanel DisplayControl    (Control c,   string panelTitle = null);
public static OutputPanel DisplayWpfElement (UIElement e, string panelTitle = null);

The advantage of these methods over calling .Dump() is that you get back an OutputPanel object (more on this later). Here's a simple example of displaying a button:

(You'll need to import System.Windows.Forms.dll - press F4 or more simply use the smart-tag if you have autocompletion.)

Here's the same thing done with WPF:

var button = new System.Windows.Controls.Button { Content = "Hello, world!" };
PanelManager.DisplayWpfElement (button, "My Button");

These controls are 'live', meaning you can interact with them after the query completes. For instance:

var button = new System.Windows.Controls.Button { Content = "Hello, world!" };
button.Click += (sender, args) => button.Content = "I've just been clicked!";
PanelManager.DisplayWpfElement (button, "My Button");

Putting this practical use, the following extension method uses a Windows Forms DataGridView to visualize a DataTable, allowing limitless rows (rather like LINQPad's results-to-grid mode):

public static class Extensions
{
   public static void DisplayInGrid (this DataTable dt)
   {
      var grid = new DataGridView { DataSource = dt };
      PanelManager.DisplayControl (grid);
   }
}

StackWpfElement

Controls that you display with DisplayControl or DisplayWpfElement fill and monopolize the entire panel. To display multiple elements on a single panel, there's another method in PanelManager called StackWpfElement.

public static OutputPanel StackWpfElement (UIElement e, string panelTitle = null);

StackWpfElement docks the UIElement to the top of the panel. Calling this repeatedly results in a Dump-like effect:

PanelManager.StackWpfElement (new Label  { Content = "Hello" }, "WPF Control Gallery");
PanelManager.StackWpfElement (new Button { Content = "Click" }, "WPF Control Gallery");
PanelManager.StackWpfElement (new Expander { Header = "More" }, "WPF Control Gallery");
PanelManager.StackWpfElement (new GroupBox { Header = "Data" }, "WPF Control Gallery");

Again, each element can respond to events after the query has finished executing.

To give a practical example, here's how you could write a method to dump IObservable objects, showing each observable change in-place:

// Note that you need to import the System.Reactive and System.Reactive.Linq namespaces for this example:

public static IObservable<T> DumpLatest<T> (this IObservable<T> obs)
{
   var presenter = new ContentPresenter ();
   OutputPanel outPanel = PanelManager.StackWpfElement (presenter, "Live &Observables");
       
   var subscription = obs
       .ObserveOn (presenter.Dispatcher)
       .Subscribe (val => presenter.Content = val, ex => presenter.Content = ex.Message);

   outPanel.PanelClosed += delegate { subscription.Dispose (); };
   return obs;
}            

(LINQPad in fact includes a DumpLatest extension method for observables that works very much like this, when called with an argument of true.)

Note that if you're looking for a Windows Forms equivalent of StackWpfElement, there is none: docking lots of WinForms controls gets slow pretty quickly! (This is why calling .Dump() on a Windows Forms control is equivalent to calling PanelManager.DisplayControl whereas calling .Dump() on a WPF element is equivalent to calling PanelManager.StackWpfElement.)

OutputPanel and GetOutputPanel

The PanelManager methods return an OutputPanel object which exposes properties relating to the output panel. It also exposes events that you can hook into, such as QueryEnded and PanelClosed (we'll see later how these can be useful). Calling GetControl or GetWpfElement on the OutputPanel returns the control or WPF element that you created it with.

You can obtain an OutputPanel instance for a panel that you previously created - simply call PanelManager.GetOutputPanel. With this method, we could reimplement the StackWpfElement method ourselves:

public static OutputPanel StackWpfElement (System.Windows.UIElement e, string panelTitle = null)
{
   if (string.IsNullOrEmpty (panelTitle)) panelTitle = "&Custom";

   StackPanel stackContainer;
   var panel = PanelManager.GetOutputPanels (panelTitle)
                           .FirstOrDefault (c => c.GetWpfElement() is StackPanel);
   if (panel == null)
   {
      stackContainer = new StackPanel();
      panel = PanelManager.DisplayWpfElement (stackContainer, panelTitle);
   }
   else
      stackContainer = (StackPanel) panel.GetWpfElement();

   stackContainer.Children.Add (e);
   return panel;
}		

(LINQPad's StackWpfElement method is slightly more sophisticated in that it wraps the StackPanel in a ScrollViewer to enable vertical scrolling).

Packaging and Deployment

Using PanelManager from within LINQPad is great for testing ideas. For more sophisticated visualizers, however, you'll probably want to create a Visual Studio project. The good news is that you can create a project in VS that references LINQPad, build a .DLL, and just drop it in to LINQPad's plug-in folder. Your visualizer - along with any extensions that you define - will be automatically visible to all queries!

By default, LINQPad's plugins folder is My Documents\LINQPad Plugins\NetCore3 (for LINQPad 6+) or My Documents\LINQPad Plugins\Framework 4.6 (for LINQPad 5). You can change this in Edit | Preferences. Any DLLs or EXEs that you put this folder are automatically referenced by all queries.

Another deployment option is to go to the My Extensions query and add a reference to your visualizer from there. All queries will then automatically reference your visualizer. The reference that you add can be either a DLL or a NuGet package.

So, here are the steps to creating and deploying a visualizer:

  1. Create a class library project in Visual Studio, and add a reference to the LINQPad.Reference NuGet package (this package works for both LINQPad 5 and 6+). For LINQPad 6, create a .NET Core project; for LINQPad 5, create a .NET Framework project.
  2. Write the desired extension methods that call PanelManager's methods.
  3. Copy the DLL and any dependencies into LINQPad's plugins folder  (My Documents\LINQPad Plugins\NetCore3 by default) - or add a reference to your DLL/NuGet package in the My Extensions query.

Advanced Features

Life Extension Tokens

Sometimes it's useful to keep a query in a "running" state for a while after the user's code has finished executing. An example is when dumping observables - you want the query to show as running until all observables have disengorged. To achieve this, there's a method in the Util class - Util.KeepRunning(). Just dispose the object that it returns when you no longer need the query's life to be extended.

Note that the user may cancel a query extended on a life extension token. You can respond to this by handling the OutputPanel's QueryEnded event, and checking the OutputPanel's IsCanceled property.

Multi-Targeting

With multi-targeting, a single project in Visual Studio can target both .NET Core (for LINQPad 6+) and .NET Framework (for LINQPad 5). When you build, VS creates a NuGet package that includes both assemblies. To multi-target, start with the following project file:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

    <PropertyGroup>
        <TargetFrameworks>netcoreapp3.0;net46</TargetFrameworks>
        <UseWpf>true</UseWpf>
        <AssemblyName>YourAssemblyName</AssemblyName>
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    </PropertyGroup>

    <PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
        <DefineConstants>NETCORE</DefineConstants>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="LINQPad.Reference" Version="1.*" />
    </ItemGroup>

    <ItemGroup Condition="'$(TargetFramework)' == 'net46'">
        <Reference Include="System.Xaml">
            <RequiredTargetFramework>4.0</RequiredTargetFramework>
        </Reference>
        <Reference Include="WindowsBase" />
        <Reference Include="PresentationCore" />
        <Reference Include="PresentationFramework" />
    </ItemGroup>

</Project>

When you build, Visual Studio automatically creates a NuGet package whose lib folder contains net48 and netx.0 subfolders (net48 for LINQPad 5 and netx.0 for LINQPad 6+).

You can then publish this NuGet package and reference it either from an individual query, or globally via the My Extensions query.