Building User Interfaces with LINQPad

...
  • By Ivan Gavryliuk
  • In LINQPad  |  UI
  • Posted 14/01/2020

Building User Interfaces with LINQPad

LINQPad is awesome! This is an extremely lightweight tool for writing quick code snippets with C# and executing them almost instantly. It's also completely free, with options to enable more feature when you buy it, and you absolutely should and will when you start enjoying it as much as I do (I'm not affiliated with LINQPad in any way!).

Apart from quick prototyping, which you as a fresh LINQPad user, or a seasonal one who never wondered what else it can do, LINQPad supports tons of other features. My favourite one are Dump method, which can display anything to the output without spending a lot of time writing logging code or digging in the debugger (maybe another post on this later), and building simple user interfaces.

Why UI is even relevant

You might ask that, of course. In my opinion and experience, UI is great in snippets when you need some intervention in how a program executes. For instance, supplying snippet input without actually hardcoding one, or deciding how program should execute further after it has reached a certain point. You might also want to share your snippets with other people, without hardcoding something sensitive and not worrying that you've forgot to remove it from the code.

I'll put a few example scenarios here so that you can recognise the power.

Sample 1. Analysing Images with Computer Vision

One day I was trying to play with Computer Vision API to see what I can do and of course I've chosen LINQPad. The instructions how to use those API are self-explanatory, so I've created a simple code snippet and it actually works.

Now I'd like to play with a lot of images, and SDK support supplying them either via internet URL, or by uploading a stream. Changing the URL all the time in code, or putting a file path quickly annoys me, therefore UI would be a perfect fit for it. All I need is a textbox for putting in a URL, or a file picker from local disk.

Fortunately, LINQPad has controls to support that. Therefore, to add a textbox to type an image URL I can type var fileUrl = new TextBox().Dump ("Image URL"); and to add a file picker, there is a built-in control, therefore this will do var filePath = new FilePicker ().Dump ("Pick a file...");. I also need a button to kick off the image analysis, and some area to display results. LINQPad has something called DumpContainers which are something like placeholders for dynamic areas in the program output you can change yourself, therefore I can add one: var results = new DumpContainer();. I can use this dump container to display the results. Here is a full code snippet for LINQPad:

static string subscriptionKey = "[computer vision key]";
static string endpoint = "https://[instance name].cognitiveservices.azure.com/";

async Task Main()
{
	var results = new DumpContainer();

	// Create a client
	ComputerVisionClient client = Authenticate(endpoint, subscriptionKey);
	
	var fileUrl = new TextBox().Dump ("Image URL");
	"or".Dump();
	var filePath = new FilePicker ().Dump ("Pick a file...");
	
	var button = new Button ("Analyse").Dump();
	button.Click += (sender, args) =>
	{
		if (!string.IsNullOrEmpty(fileUrl.Text))
		{
			AnalyzeImageUrl(client, fileUrl.Text, results);
		}
		else if(!string.IsNullOrEmpty(filePath.Text))
		{
			AnalyzeLocalImage(client, filePath.Text, results);
		}
	};
	
	results.Dump("results");
}

public static async Task AnalyzeLocalImage(ComputerVisionClient client, string imagePath, DumpContainer container)
{
	// Creating a list that defines the features to be extracted from the image. 
	List<VisualFeatureTypes> features = new List<VisualFeatureTypes>()
	{
	  VisualFeatureTypes.Categories, VisualFeatureTypes.Description,
	  VisualFeatureTypes.Faces, VisualFeatureTypes.ImageType,
	  VisualFeatureTypes.Tags, VisualFeatureTypes.Adult,
	  VisualFeatureTypes.Color, VisualFeatureTypes.Brands,
	  VisualFeatureTypes.Objects
	};

	container.Content = $"Analyzing the {Path.GetFileName(imagePath)}...";
	using (Stream input = File.OpenRead(imagePath))
	{
		ImageAnalysis results = await client.AnalyzeImageInStreamAsync(input, features);
		container.Content = results;
	}
}

public static async Task AnalyzeImageUrl(ComputerVisionClient client, string imageUrl, DumpContainer container)
{
	// Creating a list that defines the features to be extracted from the image. 
	List<VisualFeatureTypes> features = new List<VisualFeatureTypes>()
	{
	  VisualFeatureTypes.Categories, VisualFeatureTypes.Description,
	  VisualFeatureTypes.Faces, VisualFeatureTypes.ImageType,
	  VisualFeatureTypes.Tags, VisualFeatureTypes.Adult,
	  VisualFeatureTypes.Color, VisualFeatureTypes.Brands,
	  VisualFeatureTypes.Objects
	};

	container.Content = $"Analyzing the {Path.GetFileName(imageUrl)}...";
	ImageAnalysis results = await client.AnalyzeImageAsync(imageUrl, features);
	container.Content = results;
}

public static ComputerVisionClient Authenticate(string endpoint, string key)
{
	ComputerVisionClient client =
	  new ComputerVisionClient(new ApiKeyServiceClientCredentials(key))
	  { Endpoint = endpoint };
	return client;
}

When I run it in LINQPad, the following UI appears:

lp-ui1

All the interesting things happen inside the Main function:

  1. We create the DumpContainer, fileUrl and filePath where user can type stuff (we need either URL or path). The interesting thing about FilePicker class (LINQPad's own) is it actually displays file open dialog allowing you to pick using the Windows' default dialog.
  2. Depending on what information is typed in, we call either a method for analysing by URL, or with file path. You could probably shorten them as they share the same logic, which was frankly copy-pasted, but I just don't care about code quality here - remember, LINQPad is for experimentation only.
  3. The code that analyses the image is writing results into DumpContainer.

Here is how my workflow looks like:

lp-url

I've googled some image, copied it's url, pasted into the text field and pressed Analyse button. Then the code has ran and dumped the result object ImageAnalysis into the dump container. How awesome!

Same applies for local image testing:

lp-local

Summary

I needed to play with Computer Vision library, by choosing a lot of different images and analysing the results. One of the solutions would be to hardcode image path, however I didn't want to distract myself with code and get more natural feel.

I've used LINQPad's Dump method to view the results of the analysis provided by C# SDK:

ImageAnalysis results = await client.AnalyzeImageAsync(imageUrl, features);
container.Content = results;

LINQPad can automatically decompose any class and display it in a nice tree like format you can browse, you can also call it a UI.

Sample 2. Learning Azure Blob Storage Leasing

Another day, I wanted to understand how exactly Azure Blob Storage works with leasing objects. Again, writing a separate project in C# seems to be an overkill, and to be honest I'd never get to that, as it seems like a big long task. Therefore I've decided to play with LINQPad's quick editor.

I've created a blob storage account, and wrote the following code that creates a blob storage client, container, and a test blob:

string leaseId = null;
CloudBlobClient blobClient = null;
string containerName = "leasing";
string blobName = "leased.txt";
CloudBlobContainer container = null;
CloudBlockBlob blob = null;

//create client and container to play with
blobClient = account.CreateCloudBlobClient();
container = blobClient.GetContainerReference(containerName);
await container.CreateIfNotExistsAsync();
blob = container.GetBlockBlobReference(blobName);
await BreakAsync(true);
await blob.UploadTextAsync("test@" + DateTime.UtcNow);

This initialises my C# playground. Furthermore, I wrote a simple method that monitors my test blob and displays lease status every second:

private async Task MonitorLeaseAsync(CloudBlockBlob blob, string containerName)
{
	var dc = new DumpContainer();
	dc.Dump("Monitor");

	while (true)
	{
		await blob.FetchAttributesAsync();
		
		var state = new Dictionary<string, object>
		{
			["id"] = leaseId,
			["duration"] = blob.Properties.LeaseDuration,
			["state"] = blob.Properties.LeaseState,
			["status"] = blob.Properties.LeaseStatus,
			["updated"] = DateTime.Now
		};
		
		dc.Content = state;
		
		await Task.Delay(TimeSpan.FromSeconds(1));
	}
}

It would produce a result in LINQPad similar to this:

lp-monitor1

Now, there are a few options. I could use Azure Storage Explorer to play with leases and see the result in my code, however that doesn't fulfil my curious mind, as I'd like to perform all the leasing operations from code, using C# SDK. Moreover, all the leasing operations are really simple, for instance to acquire a lease I'd have to write just this:

private async Task AcquireAsync(TimeSpan? timeSpan = null)
{
	leaseId = await blob.AcquireLeaseAsync(
		timeSpan,
		Guid.NewGuid().ToString());
}

and so on. Therefore I've written small methods to perform all the operations I want. Then I used another UI helper in LINQPad - Hyperlinq, which displays as you could guess a hyperlink such as on the web, and allows you to customise it - give a display name and most importantly define a C# method which gets executed when someone clicks on it. I can, therefore, use hyperlinks to create actions to execute on my test blob, and considering I've already implemented a monitor, it should look very cool. Let's see:

lp-runhl

What you see here is me pressing hyperlinks in order to execute some actions, and monitoring routing picking up the changes every second. Pretty cool, huh? It allows me to understand what exactly is happening when I perform one action or another, see what errors are thrown under certain conditions, simulate logical workflows manually and so on - pretty powerful.

I'm putting the full source code inline:

string leaseId = null;
CloudBlobClient blobClient = null;
string containerName = "leasing";
string blobName = "leased.txt";
CloudBlobContainer container = null;
CloudBlockBlob blob = null;

async Task Main()
{
	CloudStorageAccount account = CloudStorageAccount.Parse("[storage conenection string]");

	//create client and container to play with
	blobClient = account.CreateCloudBlobClient();
	container = blobClient.GetContainerReference(containerName);
	await container.CreateIfNotExistsAsync();
	blob = container.GetBlockBlobReference(blobName);
	await BreakAsync(true);
	await blob.UploadTextAsync("test@" + DateTime.UtcNow);


	MonitorLeaseAsync(blob, "leasing");
	
	Console.WriteLine("LEASING");
	new Hyperlinq(() => { AcquireAsync(); }, "➕ acquire Lease").Dump();
	new Hyperlinq(() => { AcquireAsync(TimeSpan.FromSeconds(15)); }, "⌛ acquire Lease (for 15 seconds)").Dump();
	new Hyperlinq(() => { ChangeAsync(false); }, "change Lease").Dump();
	new Hyperlinq(() => { ChangeAsync(true); }, "change Lease from incorrect ID").Dump();
	new Hyperlinq(() => { RenewAsync(); }, "renew Lease").Dump();
	new Hyperlinq(() => { ReleaseAsync(); }, "release Lease").Dump();
	new Hyperlinq(() => { BreakAsync(); }, "💔 break Lease").Dump();
	
	Console.WriteLine("READ/WRITE");
	new Hyperlinq(() => { WriteBlobAsync(); }, "✍ write to blob bravely").Dump();
	new Hyperlinq(() => { WriteBlobAsync(true); }, "✍ write with lease").Dump();
	new Hyperlinq(() => { ReadBlobAsync(); }, "📚 read bravely").Dump();
	new Hyperlinq(() => { ReadBlobAsync(true); }, "📚 read with lease").Dump();
	
	await Task.Delay(TimeSpan.FromHours(1));
}

private async Task BreakAsync(bool ignoreError = false)
{
	try
	{
		await blob.BreakLeaseAsync(null);
	}
	catch
	{
		if(!ignoreError) throw;
	}
}

private async Task ReleaseAsync()
{
	await blob.ReleaseLeaseAsync(AccessCondition.GenerateLeaseCondition(leaseId));
}

private async Task RenewAsync()
{
	await blob.RenewLeaseAsync(AccessCondition.GenerateLeaseCondition(leaseId));
}

private async Task AcquireAsync(TimeSpan? timeSpan = null)
{
	leaseId = await blob.AcquireLeaseAsync(
		timeSpan,
		Guid.NewGuid().ToString());
}

private async Task ChangeAsync(bool fromWrong)
{
	string newLeaseId = Guid.NewGuid().ToString();
	await blob.ChangeLeaseAsync(
		newLeaseId,
		AccessCondition.GenerateLeaseCondition(fromWrong ? Guid.NewGuid().ToString() : leaseId));
	leaseId = newLeaseId;
}

private async Task WriteBlobAsync(bool useLease = false)
{
	if(useLease)
	{
		await blob.UploadTextAsync(
			$"lease {leaseId} update@{DateTime.UtcNow}",
			null,
			AccessCondition.GenerateLeaseCondition(leaseId),
			null,
			null);
	}
	else
	{
		await blob.UploadTextAsync("brave update@" + DateTime.UtcNow);
	}
	
	Console.WriteLine("updated");
}

private async Task ReadBlobAsync(bool useLease = false)
{
	if(useLease)
	{
		Console.WriteLine(await blob.DownloadTextAsync(null, AccessCondition.GenerateLeaseCondition(leaseId), null, null));
	}
	else
	{
		Console.WriteLine(await blob.DownloadTextAsync());
	}
	
}

private async Task MonitorLeaseAsync(CloudBlockBlob blob, string containerName)
{
	var dc = new DumpContainer();
	dc.Dump("Monitor");

	while (true)
	{
		await blob.FetchAttributesAsync();
		
		var state = new Dictionary<string, object>
		{
			["id"] = leaseId,
			["duration"] = blob.Properties.LeaseDuration,
			["state"] = blob.Properties.LeaseState,
			["status"] = blob.Properties.LeaseStatus,
			["updated"] = DateTime.Now
		};
		
		dc.Content = state;
		
		await Task.Delay(TimeSpan.FromSeconds(1));
	}
}

Summary

Hyperlink class is another way to display interactive UI similar to HTML ones, and react to events. In general, you can build pretty powerful experimentation interfaces.

Final Words

LINQPad is not just a simple scratch editor, it's much more than that. I've touched a small part of it's capabilities of building UI which is a powerful tool in experimentation and research. I've only looked at a few controls that LINQPad provides, however there are many more.

Hint - you can read and play with LINQPad controls from the tool itself - go to Samples -> LINQPad Controls:

lp-controlsdocs

I'd encourage you to explore all of the LINQPad features, deep deeper into controls support. Familiarise yourself with Dump - probably the most awesome invention in C# visualisation world. There is also support for charting, debugging, a lot of utilities like image visualisation, animations, async helpers and so on.

LINQPad is definitely one of the tools you should have installed on your development machine as it will make your life more exciting.


Thanks for reading. If you would like to follow up with future posts please subscribe to my rss feed and/or follow me on twitter.