Building a Stack Overflow browser as a VS extension
I have been writing a couple of integration with the Stack Overflow API for both the elmah.io app and some public exceptions pages that we launched recently (like System.DivideByZeroException). For this post, I want to show you how to pull data from Stack Overflow with C#. For demo purposes (and TBH because I wanted to play more with Visual Studio extensions), the sample code for this post will end out in a small Visual Studio extension (VSIX).
Visual Studio comes with a great set of tools for developing extensions. If you haven't developed extensions before, the chances that you have tools installed are close to zero. Luckily, there's a quick fix for that. Launch the Visual Studio Installer, click Modify next to your installation of Visual Studio and make sure to enable Visual Studio extension development beneath the Other Toolsets section:
Once the additional tools are installed, you can create new Visual Studio extension projects or VSIX Project for short:
Create a new project and name it StackOverflowBrowser:
This will generate a minimalistic project with a manifest and C# file:
I won't go into details on either file here, since there are tons of articles on these file types available already. In addition, none of the files are very interesting in relation to the browser we want to build for this post.
I want to add a tool window inside Visual Studio where I can quickly search Stack Overflow answers for a query of choice. Tool windows are the internal windows inside VS that you can either maximize or dock on one of the sides (like the Solution Explorer in the screenshot above. To add a custom tool window, right-click the project and add a new Tool Window available beneath the Extensibility group in the Add New Item dialog:
I've named the window StackOverflowToolWindow
. This will include a couple of additional files to the project:
You can inspect the various files, but the one I want to dig down into is StackOverflowToolWindowControl.xaml
. This is a classic XAML view that should look familiar if you worked with WPF, Silverlight, Xamarin Forms, or another framework using XAML as the UI engine. For the uninvited, XAML is an XML language used to build UI's for one or more platforms. In that case Visual Studio.
Adding a bit of UI is easy using the following code:
<UserControl ...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="40"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0">Query:</TextBlock>
<TextBox Grid.Column="1" x:Name="query"></TextBox>
<Button Click="Button_Click" Grid.Column="2" Content="Search"></Button>
<WebBrowser Grid.Row="1" Grid.ColumnSpan="3" x:Name="browser" />
</Grid>
</UserControl>
I have left out the attributes for the UserControl
element since I didn't change anything from what's already generated.
The markup generates a simple view that probably won't win any UI awards but get the job done:
In the first row, there's an input field to input a query and a search button. In the second row, I'm using the WebBrowser
control that comes with XAML.
You might have noticed already, that I have named some of the controls inside the XAML file as well as added a click handler on the button. This is done to react to the user clicking the Search button and adding a search result to the WebBrowser
control.
Before hooking up the UI code, we need an HTTP client to communicate with the Stack Overflow API. Add a new class to the project named StackOverflowClient.cs
and include the following initial code:
public class StackOverflowClient
{
private static readonly Lazy<StackOverflowClient> lazy = new Lazy<StackOverflowClient>(() => new StackOverflowClient());
public static StackOverflowClient Instance { get { return lazy.Value; } }
private HttpClient httpClient;
private StackOverflowClient()
{
httpClient = new HttpClient(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});
httpClient.BaseAddress = new Uri("https://api.stackexchange.com/2.3/");
httpClient.Timeout = new TimeSpan(0, 0, 0, 5);
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
}
This is a simple implementation of a singleton client, wrapping an HttpClient
instance. Visual Studio doesn't provide the usual dependency injection features for HTTP clients provided by frameworks like ASP.NET Core (at least not to my knowledge), why I have decided on a singleton pattern like this.
I want the StackOverflowClient
to search for the keyword provided when the user clicks the Search button and return some HTML to render in the WebBrowser
control. To do so, implement a Search
method like this:
private const string Error = "<h1>Error</h1>";
public async Task<string> Search(string text)
{
HttpResponseMessage result = null;
try
{
result = await httpClient.GetAsync(
$"search/advanced?page=1&pagesize=3&site=stackoverflow&order=desc&sort=votes&q={text}&accepted=True");
}
catch
{
return Error;
}
if (!result.IsSuccessStatusCode) return Error;
var bodyString = await result.Content.ReadAsStringAsync();
dynamic body = JObject.Parse(bodyString);
var questionIds = new List<string>();
foreach (var item in body["items"])
{
questionIds.Add(item["question_id"].ToString());
}
if (!questionIds.Any()) return "<h1>No results</h1>";
try
{
result = await httpClient.GetAsync(
$"questions/{string.Join(";", questionIds)}/answers?order=desc&sort=activity&site=stackoverflow&filter=!ao-)iqhlSQw3mu");
}
catch
{
return Error;
}
if (!result.IsSuccessStatusCode) return Error;
bodyString = await result.Content.ReadAsStringAsync();
body = JObject.Parse(bodyString);
var sb = new StringBuilder();
foreach (var item in body["items"])
{
var isAccepted = item["is_accepted"] == true;
if (isAccepted)
{
sb.Append(item["body"].ToString());
sb.Append("<hr/>");
}
}
return sb.ToString();
}
Let's go through the code section by section. In the first section, I use the HttpClient
instances created in the previous step to search Stack Overflow for questions related to the query provided through the query
parameter for the Search
method. If you inspect the query parameters for the Stack Overflow API, I provide various options like a page size of 3 to get the 3 most relevant questions and sorting the results by votes. The easiest way to figure out which parameters to use is to play around with making requests on Stack Overflow's official documentation that contains a play area on all endpoints: https://api.stackexchange.com/docs/advanced-search.
Next, I parse the results returned from the search endpoint and collect the IDs of those questions. These serve as input for the second HTTP request made to the answers endpoint. Requesting this endpoint will return all responses to the questions found in the previous step. Like in the previous step, I'm including a range of query parameters for the Stack Overflow API. The only parameter looking weird is filter=!ao-)iqhlSQw3mu
. If you are coding along and don't want details about how the API works, you can simply copy the code and go on with your life. For the curious, this is a checksum of fields I want to return from the Stack Overflow API. You can generate your own checksum by clicking the edit link on this page: https://api.stackexchange.com/docs/answers-on-questions.
In the final step, I run through the answers and add the value of the body
field to a StringBuilder
. body
is a field returned by the Stack Overflow API containing the answer in HTML.
That's it. The last step is to call the StackOverflowClient
from the button click and fill in the HTML result in the WebBrowser
control. Open the StackOverflowToolWindowControl.xaml.cs
file and include the following click handler:
private async void Button_Click(object sender, RoutedEventArgs e)
{
var stackOverflowClient = StackOverflowClient.Instance;
var result = await stackOverflowClient.Search(query.Text);
browser.NavigateToString(result);
}
Clicking the button now calls the Search
method that we added in the previous step and set the generated HTML on the WebBrowser
control using the NavigateToString
method. Before we move on, I want to put a few words on the async implementation in the code above. Declaring the Button_Click
method with async void
is not the recommended way to implement async in XAML and/or Visual Studio extension. Since I don't want too many details about this in this blog post, please check out https://github.com/Microsoft/vs-threading/blob/main/doc/cookbook_vs.md for more information.
Let's launch the extension and see if everything works as expected. When using the wizard in Visual Studio to add the project as a VSIX, the project is already configured to launch the extension in a new (experimental) instance of Visual Studio. Hit F5 and search for StackOverflow in the search field:
Launch the tool window and input a query of choice:
Yay! We now built a Stack Overflow browser inside Visual Studio 💪 We would need more info in the window as well as links back to Stack Overflow to use this as part of developing inside VS. I'll leave these missing pieces up to you.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website