Porting a Windows Forms Application to .NET – Part 1

In a time when every dinky software company is writing massive systems that run in The Cloud™, it’s a source of astonishment for some to hear about my continued work on a desktop application. It’s not quite the same effort as synchronising 5K posts per second with millions of followers. But it does have its high notes nevertheless.

Understanding the Legacy

Like many products from early-stage startups, the first iteration of the Vertika Player was built as a proof-of-concept. The objective was to fetch text, video and images from a web service and play it on a large screen. But the underlying Windows Forms framework wasn’t really geared towards rendering multimedia content. So a separate application was written in ActionScript, which was hosted inside the Windows Forms application through the Flash Player ActiveX control.

Even during those days, web development was topping the list of desired programming skills. Nobody on the original team had significant experience engineering a desktop application. As a result, it came to be that most of the code got piled into the main window class, a couple of static utility classes, and a single god class containing approximately 200 public fields to maintain application state. The latter was referenced hither and yon all over the project. It was awful. The project was so fragile that even changing a simple network port number was fraught with risk of breakages in parts of the application that had nothing to do with networking.

Apparently, none of the programmers understood the significance of the UI thread, or even basic threading in general. Resource contentions were handled with a hail Mary running inside an infinite loop.

while (!result)
{
    try
    {
        xmlDoc.Save(FilePath);
        result = true;
    }
    catch (Exception exception)
    {
        log.Error("An error occurred while saving to disk");
    }
}

There were some hard to replicate bugs that reared their heads at times. Support incidents came up occasionally about missing content, or false alarms were raised when the telemetry and monitoring thread went awry. A particularly egregious bug was the cause for occasional runaway file downloads that never stopped, appending the same sequence of bytes in a file ad infinitum. To the end user, the file would seem to grow continuously until it filled the entire hard disk.

But in spite of all these issues, the application worked as expected more often than it didn’t. It generated steady revenue from paying customers who were mostly pleased with its performance. Support staff had formulated scripts for almost every scenario that they were likely to encounter. They could identify symptoms and were able to get in and resolve the problem quickly.

Ringing in the New

Teams changed, until there was nobody left who had worked on the first iteration of the product.

New features were gradually added over time. But architectural issues continued to fester, while more lines of code were added to already expansive classes. At one time, the main window class exceeded 5,000 lines of code. It wasn’t a pleasant experience maintaining this project. But the one thing that we all agreed upon was that we would never discard the project and start from scratch. We bided our time and waited for an opportunity to refactor the application.

We got our foot in when customers started reporting stutter during playback after minor update. Telemetry reports had to include a screen shot of the running application. But due to data caps at many locations, captured images used to be down-sampled to postage stamp sizes before uploading. Network capacities had recently been improved, and screen shot dimensions were increased accordingly.

Herein lay the catch. A 100 pixel square image is about 2 KB in size. But a 500 pixel square accounts for a 25x increase in data. The code to upload the image was being executed on the UI thread, blocking it while the network request was completing. This was imperceptible when the file sizes were small. But it became immediately noticeable once the images sizes were increased. It was glaringly obvious during playback of video because of periodic frame freeze that occurred while an image was uploaded.

We chose the expeditious fix for the time being. Instead of directly calling the upload method, it was wrapped into a WaitCallback delegate and queued for later execution through ThreadPool.QueueUserWorkItem(). The UI thread was freed up again, solving the playback stutters. But we made dozens of commits in the project while working our way to these few lines of change. This was the chance we were looking for. We had to get the cat out of the bag.

All our early modifications were mechanical and low-risk. Auto-format the code. Reduce the API surface area by hiding unnecessary public members. Remove dead and commented lines of code.

This exercise gave us the exposure to the codebase that was necessary to make further modifications. Further refactoring was driven by some of the SOLID principles. If we found a class that was doing too many things, we’d try to split it into different types with individual responsibilities. Interfaces were scarcely in use so far. So we began to define some new ones, being careful to contain their scope. But dependency inversion wasn’t quite as high on our list of priorities. .NET Framework didn’t ship with built-in DI features, and adding a third-party library for the job would impede our path to upgrade to .NET Core later down the line.

Over time, the quality of the codebase improved, and our knowledge of its internal working increased. There were few changes which were visible to the end user. In general, it was just a little more robust, faster to launch and run, and the UI looked a bit tidier.

The Demise of Flash

Come January 2021, disaster struck. Flash had been on its last legs since a long time. But nobody expected Adobe to pull the rug from under us by forcefully removing the plug-in from all computers. This bricked every deployed instance of the application. Nothing could be displayed on any screen without the Flash Player.

He’s dead, Jim.

We scrambled to find workarounds, such as installing older versions of Flash Player that did not contain the the self-destruct switch, blocking Windows Updates, and even deploying older versions of Windows that shipped with working versions of Flash. These were all temporary solutions. The juggernaut was unstoppable, and we were fielding support calls every week that involved a missing Flash Player. In addition to the technical complexities, Adobe had also revoked the license to install and run the Flash Player. Continuing to use it was exposing the company to legal ramifications that nobody wanted to deal with. We needed an alternative for Flash as soon as possible.

Blazing Guns of Glory

Our salvation came in the form of the recently released Blazor framework from Microsoft. Built on the C# language, it borrowed a lot of concepts from ASP.NET and MVC. A crack team of developers was assembled, with the singular motive of porting the entire Flash application into Blazor. This was a significant diversion from our ground rule to never throw away everything and start from scratch.

Blazor generates a web application that runs in a browser. Embed a browser control into a Windows Forms project, and voila! It works just the same as the Flash application. The only significant difference was the communication protocol across process boundaries. Flash used to expose a TCP sockets API to communicate with the host process. Since web browsers don’t expose any such interface, we had to fall back upon standard HTTP. The hosting process would expose a web API that would be accessible on localhost over the HTTP protocol.

The last item was a significant pivot point. Hosting a HTTP API would require a web server, for which there were limited options that worked within the .NET Framework ecosystem. HttpListener was far too low-level. Cassini wasn’t as tightly integrated. GenHttp came with its own learning curve. In the long-term, it was going to be far better to switch from .NET Framework to .NET 5. And it shipped with its own built-in web server that supported running ASP.NET Web API out of the box.

The writing was on the wall. We would be porting to .NET 5.

Valknut – JSON Serialization

Many service responses are returned as JSON objects. For this, the application must implement some basic configuration and error handling in each method. By utilising the cross-cutting facet of attributes, this code is unified into a common location and applied wherever required.

Signalling

Firstly, the action method that returns JSON has to be marked. The JsonHandlerAttribute class inherits from ActionFilterAttribute. It is applied to the method in question. When the method is invoked, the ASP.NET pipeline executes the OnActionExecuted method on its attribute. The method converts the System.Web.Mvc.JsonResult result into a custom-written class called FitNet.Web.Infrastructure.JsonNetResult. This class inherits from JsonResult and adds some shared functionality.

Error Handling and Serialization

The conversion from JsonResult to JsonNetResult is performed inside the JsonHandlerAttribute class. The Result property of the current filter context is typed into JsonNetResult. If the conversion is successful, then a new instance of JsonNetResult is created and the copies of the current result copied over.

Finally, the ExecuteResult method is invoked on the newly created result instance. This step performs the following error checks.

  1. Deny GET requests by default, unless it is explicitly allowed in the method.
  2. Set the content type of the response to application/json.
  3. Configure the underlying serializer to throw an error in case of looping references.

If all the checks pass, then the response data is serialized and the output is assigned to the Output property of the current response.

This structure of the code removes the need to have all these checks in each method that has to return a JSON response.

Valknut – View Models

The application utilises view models heavily for all kinds of client-side communication. A page view model is an obvious candidate. But even JSON requests and responses are deserialised into a view model for better server-side manipulation.

Core Hierarchy

The application view model architecture is highly formalised. It follows a strong hierarchy for transmitting the various kinds of data models to the client and back. The base of this entire hierachy is the class FitNet.Web.ViewModels.EntityViewModel. This class exposes the properties Id and Name.

All entity view models which are used to enumerate records, show editable forms or delete rows inherit from EntityViewModel. This offers the structure required to perform common sets of actions on these entities irrespective of their individual types and properties.

Page View Model

This is a generic class that is designed for transmitting data for a standard HTML output. It has a single generic type parameter that has to inherit from the EntityViewModel. This class exposes the properties Title, Permalink and Entity. The type of Entity matches the type of the generic parameter.

All entity controllers can use this class to generate an editor page with a shared set of page properties and layout hosting a form with entity type-specific fields.

DataTables View Models

DataTables is a popular jQuery plugin for rendering grids on the client-side. It supports dynamic retrieval of data, page-wise splitting, record offsets and many other features required for viewing collections.

This library is utilised heavily in Valknut, and as a result, the back-end provides built-in view models for data retrieval and submission directly through DataTables.

The classes required for this feature are listed below.

  • FitNet.Web.ViewModels.DataTables.RequestViewModel
  • FitNet.Web.ViewModels.DataTables.ColumnViewModel
  • FitNet.Web.ViewModels.DataTables.OrderViewModel
  • FitNet.Web.ViewModels.DataTables.SearchViewModel
  • FitNet.Web.ViewModels.DataTables.ResponseViewModel

Request View Models

This is a composite class that organises several other view models into a single instance. When the plugin makes an AJAX request, the request is serialised into this type. It’s native properties are Draw, Start, Length and Error. These properties are required for cache-busting, record navigation and error handling.

The Search property contains an instance of the SearchViewModel, while Columns and Order are collections of type ColumnViewModel and OrderViewModel respectively.

Server-side Processing

An AJAX request from a DataTables instance can be processed on the client-side or on the server. In the former scenario, all data is retrieved in a single network operation and stored in memory on the client. This is convenient and quick for small data sets.

But if your data sets are large or contain an indefinite number of rows, server-side processing is more efficient. For this, the plugin sends some additional information with each request.

  1. Column Data
  2. Column Name
  3. Column Searchable
  4. Column Orderable
  5. Column Search Value
  6. Column Search RegEx
  7. Column to Order By
  8. Direction of Ordering
  9. Record Number to Start At
  10. Length of Data Set
  11. Global Search Value
  12. Global Search RegEx

Column and ordering parameters are an indexed collection. The entire request is URL encoded. An example of the body sent is shown below (with the request split into multiple lines for legibility).

draw=1&
columns[0][data]=Name&
columns[0][name]=&
columns[0][searchable]=true&
columns[0][orderable]=true&
columns[0][search][value]=&
columns[0][search][regex]=false&
columns[1][data]=Id&
columns[1][name]=&
columns[1][searchable]=true&
columns[1][orderable]=false&
columns[1][search][value]=&
columns[1][search][regex]=false&
order[0][column]=0&
order[0][dir]=asc&
start=0&
length=10&
search[value]=&
search[regex]=false

Column View Model

The column fields are deserialised into an instance of this class. Each column has its own instance, and each instance has the properties Data, Name, Searchable, Orderable, Start, Length, as well as a nested instance of the search view model.

The server can utilise these values to perform any additional ordering or filtering on the result data set before dispatching it back to the client.

Order View Model

This is a very simple class consisting of only two properties – Column and Direction. The first is the index of the primary sorting column, and the second is a string containing the value asc or desc.

The server can utilise these properties to sort the data on the server.

Search View Model

This class contains the properties Value and RegEx, which is used to transmit keywords or regular expressions to the server. The data set can be filtered against these parameters before returning it to the client.

Response View Model

In addition to the collection of records, the DataTables instance also requires some additional information such as the current page number. This view model implements properties for these values.

The complete list of properties are Draw, Start, RecordsFiltered, RecordsTotal, Search, Error and Data. The last one is an ICollection instance and contains the rows to be rendered.

Valknut – Validation

Introduction

ASP.NET MVC has always had a robust request validation model that is easily harnessed. The model in question has to be decorated with validation attributes from a broad collection of possibilities, and the framework takes care of ensuring compliance and distilling it into a single boolean that can be referenced from ModelState.IsValid.

Nothing could be easier.

However, this approach requires that the programmer must remember to write the code to perform a validation check in each action method. This is tedious and error-prone, and results in duplicated code.

It is possible to implement this functionality in a more modular fashion by implementing it as a filter attribute and plugging it into the ASP.NET request pipeline.

Passive Attributes

Before we go into the implementation of the filter itself, there is a slight design idiosyncracy that has to be understood. Request filtering through a class that derives from ActionFilterAttribute is a common enough pattern. A request is filtered through this attribute if the method implementing its corresponding action is decorated with this attribute. But this approach imposes various technical restrictions and design compromises.

A more robust approach is to decorate the action method with a non-behavioural attribute that derives from Attribute, and adding the filter directly into the global filter collection.

Passive attributes are described by Mark Seemann at the link below.

Passive Attributes

Implementation

The passive attribute approach results in an attribute class called ValidateModelAttribute. Any method that requires model validation can be decorated with this attribute.

[HttpPost]
[ValidateModel]
public ActionResult Edit(PageViewModel<ExerciseEditViewModel> viewModel)

A RequestValidateModelFilter class implements the IActionFilter interface and is added into the global filters collection during the Application_Start event. When an incoming request arrives, it is passed through the RequestValidateModelFilter instance, which checks if the request requires validation, and if so, checks if the IsValid property of the model state is true.

If the model state is not valid, then the request pipeline is truncated and all validation errors are gathered into a JSON response. The application returns a HTTP status code 400 along with the list of errors in the body of the response.

Valknut – Localisation

Client-specific Regional Settings

Request Structure

Valknut implements localisation of temporal data to match the client’s preferred locale through the use of the Accept-Language header in an incoming request. The value of this header can be an array of one or more language tags, such as en-US, along with a weightage indicator for the language.

This value is set automatically by the browser based on the regional settings of the operating system, and can be overridden by the user to suit their needs. The language tag may be followed by an optional quality factor parameter that indicates the preferred weightage to the given language tag. The value of the q-factor is a decimal number between 0 to 1.

en-US;q=1,en-GB;0.5

The above example indicates that the client prefers American as well as British English, with a higher preference for American English.

An example of an entire Accept-Language header is shown below.

Accept-Language: en, en-GB;q=0.8, hi;q=0.7

This is equivalent to having the value en;q=1,en-GB;q=0.8,hi;q=0.7.

Server-side Extraction

On the server side, the framework parses the header and automatically populates the UserLanguages property of the request object. The type of UserLanguages is string[], and its entries are sorted in descending order by the quality factor. In the example above, the language tag en is the first entry, and hi is the last entry.

Once the request language has been determined, the method checks if it is whitelisted in the allowedLanguages parameter. It first checks if the entire language tag and its subtag are present in the whitelist, or failing that, if the language tag alone is found. That way, if the application only supports French language localisation, but the request contains the tag fr-CA, the response is still localised to the French language, even though it does not contain Canadian region-specific date or number formatting.

In spite of all this, if the method is unable to determine the preferred language from the request, it applies the culture that is currently active on the operating system.

Request Handling

The server matches the current culture of the thread fulfilling the request to a value from this array in order to process the request in a culture-aware manner. Currently this extends to temporal data, with scope for future support for UI string localisation, formatting of numerical data, units of measurement and iconography.

The code to configure this for the current thread is written in an extension method on the System.Web.HttpApplication class.

void SetLocale(this HttpApplication application, 
    string culture = null, 
    string uiCulture = null, 
    bool setUiCulture = true, 
    string allowedLocales = null)

This method is invoked from the Application_BeginRequest event handler, because the locale has to be set separately for each request. The SetLocale method is called directly from this event handler. The method is invoked without any parameters, which indicates that the preferred locale has to be inferred from the request headers.

If the Accept-Language header is not present in the request, the method falls back upon the regional settings configured on the server to handle the request.

The Thread class has two properties to set the culture.

  1. CurrentCulture, which determines the writing system (LTR or RTL), calendar, string sorting and formatting of date and time strings.
  2. CurrentUICulture, which determines the resource file used by the Resource Manager to localise the strings, icons and other non-code assets for the application.

This separation allows applications to be able to handle regional settings for data even if it does not support region-specific UI labels. The SetLocale method sets both properties to the same culture unless the value of setUiCulture is false.