I have decided to make a simple solution, which somehow mimics real world application, and to change it several times, using different UI technlogies and keeping the specific logic the same. I have created simple dotnet solution consisting of WinForms client and .NET core library. Then I created WPF client. Then I created Http.Sys server project which uses the library. From this point both desktop clients were making HTTP calls to Http.Sys server instead of calling the library directly (as before). Finally I created express.js node.js hosted solution which host a web application client, making calls to the Http.Sys server. Because node.js server and Http.Sys server are running on different domains (ports in demo), cross-origin restrictions are present. Most browsers restrict cross-origin HTTP requests. So I decided not to implement CORS (Cross-Origin Resource Sharing) on Http.Sys server, which is pretty fine solution, but to implemented http proxy in node.js for the Http.Sys server. So finally none of the clients were seeing the Http.Sys and they were working only with node.js server. Every change which I made solved an existing challenge and it demonstrate usage of a new technology. And of course, it introduces new challenges to solve...
Entire solution is created from scratch. It will require some typing and some work. If there is a place, where you give up reading it and move to another reading, then this one is the best. This article will not explain the code line by line, you need to read the code itself and figure it out. Of course, if there are questions, I will try to answer them to my best knowledge.
The idea of the article is to show how an application can be changed from single user desktop solution to multiple user locally hosted solution, accessible all over the web*, supporthing both web and desktop clients and even providing kind of web api. I wanted demo to be useful and for that reason I have created an app, which can do statistical classification and I am using really simple way of doing it. So the main idea is change - functionality stays the same - same textboxes and same buttons do the same thing, only UI technology is changing and when needed the related libraries. Logic stays the same - text processed in part one gives same result as same text processed in part four.
In machine learning and statistics, classification is the problem of identifying to which of a set of categories a new observation belongs, on the basis of a training set of data, containing observations which category membership is known. Our solution can be used exactly for that. However, this is a general reading programming article, so I will not use statistical and machine learning terms in it.
So, our functional view is like that: Users provide some text. Then this text is processed – some characters are removed, and remaining text is spitted into words. Those words are then placed into dictionary and ranked based on their occurrence in text. Word with most occurrences is ranked first, those with second most occurrences ranked second etc. Then users can decide whether to save the data or not. With time, when more and more text is processed and saved, the data grows. So, when users provide new text, the new text is processed and decomposed into words and then compared with existing data from previous inputs. The words who has similar ranking in both sets are colored, so that users can easily identify them. If ranks are very close, they are colored in one color, if they are not so close, they are colored in different color etc.
Then I removed words like, and, to, the, of...(generally known as determiners), then I removed most of the verbs or words which are not IT terms. Result is something like that, courtesy of WinForms client:
Words which are very close are colored in green – word experience rank 0 in existing set and rank 2 in test set, those who are close are colored in yellow and those who are not so close but still in the game – in light gray. As we can see, the test text is quite similar to our existing set.
Now, let’s go and try with another job post, but this time with different search term, like sales or HR (sales in screenshot).
Now, let’s try with something completely different – for example current Top 100 number one song lyrics or car sale post or news report or something else (top 100 # 1 lyrics in screenshot):
No colors, no match. It appear that test text and our existing set does not have much in common.
This solution was created in spring of 2019, so the current versions at this time of all products and corresponding NuGet or NPM packages were used. For the .NET solution, the following IDE was used:
and the following NuGet packages has been installed, together with their dependencies:
For node.js solution the following IDE was used:
the following npm packages:
and the following CSS style framework:
For all clients “Josefin Sans” font was used as default font. For Windows clients you need to install the font in order to use it, for web client you can reference it from google provided url. The font is available for download from google fonts site. For testing the Http services it might be useful to install application like Postman, Fiddler or similar. Postman was used during this demo. Demo solution can be completed by using other IDE’s, so this is kind of personal choice. Now it is time to start.
The first version of the .NET Framework was released on 13 February 2002. More than 17 years from the days of writing of this article. Windows Forms or as they are also commonly called WinForms were included in .NET 1.0 release. So it is proper to start our demo with this UI technology. Our project will target however .NET Framework 4.7.2. Even that .NET Core 3 Preview 5 is available at the time of writing, we don’t have an official .NET Core 3.0
We right-click the solution and select Add → New Project. From the menu Add a new project we select Class Library (.NET Standard) and we choose Next. We name our project Business and we confirm with Create. We delete
Class1.cs and we create two new classes, Word and WordProcessor. We want that our application process text and turn it into words, who have a score and grace to this score they can be ranked in certain
way. We are going to create a class, which fulfill those real world requirements. The class will be called
Word and it will look like that:
We will save all of the words that are designated to be saved by users. Still, for some sampling not all words will be necessary. So, the Show property will be used in this case. We will filter word returned to client by this property. Now, the most important class, the WordProcessor. This is the specific logic of the sample, and if we look one step further and imagine this is a real application, the class(es) will consist the specific business rules which every (small) company is using. Those rules are probably going to change in some way in future. In ideal case we would like that this change not affect the clients they continue to work with same method definitions as before. So we will make our business methods little bit more general – we are going to return interfaces instead of classes or void.
This is how we are going to implement the
WordProcessor class. For this implementation we decided to use Newtonsoft.Json for serialization and deserialization. For better
performance I would suggest using protobuf-net. I choose json in this case becasue it's easier to read saved data and later to examine services output in postman or in browser.
We are now ready to build the library and to continue with WinForms client.
Working with WinForms means in most of the cases working with the designer. Usually controls are drag and dropped from a toolbox into a panel, which is placed into the form. Working with raw designer cs/vb files is also a common practice, especially when the form is big and contains lots of controls, but in our article we are not going to employ it. So:
Then we go back go data grid menu and show it. Then we select Edit Columns… We mark Name and Count columns as read-only. We hide Rank (Visible set to false) column for now, it might be useful to show it when you want to verity the ranking and coloring further in the demo. We specify widths of columns which suits us well. We confirm with OK.
It remains only to specify the data grid font. We call the DefaultCellStyle CellStyle Builder dialog by clicking the .. button. We change the Font to Josefin Sans Regular 10 pt, whichwill be shown as Josefin Sans, 9.75pt.
We confirm with OK. We save.
It remains to specity the TabIndex for every control. rtb should have TabIndex = 0 and for the other controls we specify the following sequence: dvgLeft, dvgRight, btnPreview, btnSave, btnUpdate Our form should look something like that (or the way you want to be, if you decide to change the design a litle bit):
Now it is time to create code-behind logic, which will determine how our application works, at this moment of the development of the entire solution. We want the application to behave in the following manner:
We want few additional things.
WinForms do not support by changing of controls location and size during form resizing. Form can be dynamically resized, but controls will be not. So we will have something like that:
We can solve this in various ways - in production application we can forbid our forms to be resized or we can use some third-party control library, which provides this functionality. Those custom controls will be able to automatically fix the layout on resizing. Or we can create our own implementation of custom panel, which automatically arrange its child controls. Or we can create custom method, which arrange layout for this form in particular. We are going to use the last approach. Using it can lead to errors and will reduce maintanability in production application. But in our case it will show one advantage of a later technology, WPF, which we doing to demonstrate in greater detail in part 2. Anyway, this is how our custom method fix resizing:
Even that in today's software industry it is innovation that is respected, but not tradition, we will honor previous WinForms developers and we will name our event handlers and arguments in a specific way, even only for a small demo app. The designer names the event handlers in the following way - NameOfTheControl_EventName, and all event arguments are called e. We will use the naming On + NameOfTheControl + EventName and we will name EventArgs - ea, KeyEventArgs - kea etc. According some guidlines, worlds like Please should be ommited, if from users it is required to do something, that application would normally accept, like "Enter Password" or even "Password" instead of "Please, enter your password". This cue banner message starts with "Please" because in my native language well written instructions start with please, even it is sometimes the only choice or it imperative. And finally, this is the code-behind for frmMain
We build and run the solution. Application should be working fine, if we have created everything properly. Now let's to do the Part One live demo and close the first chapter.
Good job! We have completed part one of this article! We have created a WinForms client and .NET Core library and we have processed some text. Let's recap the good things:
And now let's recap the things which can be improved:
It's time for Part Two - Making the WPF client
Windows Presentation Foundation, WPF, was included in .NET Framework 3.0 release, on 21 November 2006. Four and half years after WinForms and more than 13 years from the days of writting this article. As WinForms, WPF is for making windows GUI applications. There are differences between the two and one of those differences is that WPF uses a mark-up language, called XAML, for definng the UI, as opposed to forms resources and code-behind designer generated files in WinForms. WPF development can be done like WinForms, with lots of code-behind code, or using a pattern, called MVVM, Model-View-ViewModel, or using a mixture between two. We are going to use the MVVM approach in our demo. When using MVVM model view basically does not care much about view model and view model does not care much about the view, they are loosely coupled. We can specify a command name and parameter in the view, but view model does not care whether the command was called by a key stroke, by pressing a button, by clicking a menu or by toggling a toggle. Similar, view model provides a collection or collection view source view, but does not care whether this is used as Item Source (for example) of a combo box, listbox, listview, datagrid etc. So, we can almost independently working on views and on view models and later just bind them. Or a designer can work on the View to make it really cool and developer can work on view model. Or..., ok you get the idea.
The focus of our demo solution is not WPF, so we are not going to use additional UI libraries and we are not going to implement content-based navigation or some little bit more advanced stuffs. You can browse the net for such examples. Simple content navigation is described in one of my other articles, Easy prototyping
We continue with our demo project and add class called ViewModelBase to ViewModels folder. We are going to add some code there and this is now it looks after we are ready (we changed the namespace name to the main one):
OK, cool. Now it is time to add a
ICommand implementation. We add the class SimpleCommand to our Infrastructure folder.
Thank you, Josh Smith. Here is it:
It is needed only because we have a custom Cue Banner, which relies on textbox enter and leave events, and they are passed to commands more easily with this package. Latest published date is March 10, 2013, nearly six years ago.
We are ready with the basics for this part, let's move forward.
As we already have a really good working WinForms apps, we just need to "translate" code from WinForms to our project. Some of the code will be unchanged, but some of it will change. We have to decide on those stuffs:
Wordclass is not ours to tell, it belongs to Business. One way to solve this is to create a small view model for word, in which we will add few (one) additional properties, needed only for this project.
Time for the code. Here is the enum WordDistanceEnum. We place the file in infrastructure folder, becasue we don't have designated folder for enums.
and here is WordViewModel, placed in View Models folder
and here is the view model for the main window. We called it MainWindowViewModel. It is more than one screen long, so it is easier to paste it directly:
Time for the mark-up. Before we go to this, there is one thing remaining - we open the code-behind files of App.xaml, App.xaml.cs and MainWindow.xaml, MainWindow.xaml.cs and we remove all non-used references and clean all default comments made by wizard. We save and build the project.
Let's make a summary about the main view - we have items controls for current and history word sets - we are going to use list views for them, we have simple item template and triggers for it and we have triggers for GotFocus and LostFocus (and forms Closing event). We have the view model as a data context set directly in the markup. Below you can see the full XAML*
Resources and interactivity triggers are collapsed so that the first screen-shot fits on one screen. You can find them expanded below.
This is how the finished project looks like, together with WinForms and Business projects. Both WPF and WinForms projects reference the Business library project. Time for small live demo for WPF client and then we close this part.
Good job! We have completed part two of the article! We have not only WinForms, but also a WPF client, working fine with our business library.
The good things for client said in end of part one hold for this WPF client as well - It works and it does the job propertly. And we have a robust UI with proven technology. We have resolved some of the things, which we wanted to improve in part one - we don't need to invent any methods for layouts rearangement when window is resized, WPF do this for us, because we set relative column sized. We use commands and we don't create "manually" events for every user action. And we don't deal with row indexes when we do the colloring of the matching words. Our solution is more robust. Maybe it has become more complex. When we talk about commercial applications using commercial third-party controls and components, WPF or WinForms is actually a personal choise for the developer, both can produce excellent applications.
Still, one of the main problems remains - only one user can create a word set at the time and multiple users cannot use the same set simultaneously. It is still single user application. So this will be our goal for part three. We can easily solve this by using a database and use it to handle multiple users. This is perfectly fine solution and the common case in company owned network. It will be risky however to allow users to connect directly to a database over the Internet. So we are going to create a web server instead, which will provide our business logic to various clients via http connections.
From WinForms and WPF we move to ASP.NET Core. Main reason for using ASP.NET Core is because our business logic is written in C#. C#/VB.NET are great for making business logic. However, if we want to use other
technology for the web, something not .NET related, we have to think about a way to access the existing logic from the other server. This will complicate the solution. One other option will be to re-write the existing
logic using other language. Even for our simple logic this will not be a trivial task, for a real-world stuffs it might happen to be very difficult. The main reason we used C#(for VB.NET is the same) back in part one
and two was the ease of creating a native applications for Windows. Still, the C# for the web and the C# for the desktop are different, event that the language looks the same (meaning C# is standartized and its
specification is the same for everybody), it feels different. The frameworks and tools are written by a different people, the way of writting feels different. Maybe they are not approaching each other with My
kung fu C# is stronger than yours, but their code feels different. There are C# developers which concatenate strings with + sign, others uses
string.Format with so-called composite
format strings, and others use interpolated strings. Reasons for that vary and one of them is because they come from other languages/technology background and are accustomed to one way or another. This 'discussion'
goes outside the scope of the article and we stop it here. But we promise to thing about using other technology (node.js - I am talking to you) in our solution and we will think again in part four. Now, let's go back
to this part and ASP.NET Core.
ASP.NET core ships with three options for web servers - the Kestrel server, which is the default, cross-platform HTTP server, IIS HTTP Server,
in-process server for IIS, and Http.Sys server, Windows-only HTTP server. I wanted application, which self-host the web server and it was easier (for me) to achieve this using the third option, the Http.Sys. In
this part of the article, we are going to create server for handling http calls, GET and POST request mostly, and one proxy project, which will use
HttpClient for making calls to server and implementing
the same methods, which WinForms and WPF clients expect, as if they were working with
WordProcessor class from Business library directly.
We are going to install four NuGet packages:
We are not going to use transport security in this demo, neither authentication or authorization. We are going to have three endpoints, current, history and save, which will respond to few HTTP verbs - GET, POST and PUT. For the others we will return default messages. Response from server will be provided as "application/json" (RFC4627). Our server will use an object with state, this will be the instance of WordProcessor. It will remain in memory. For our demo performance of this will be just fine - we will be avoiding usage of save method directly, because this method serialize the content of the dictionary to the disk and it can be the only slow thing, especially if called multiple times. More of this in part four. Finally, if you want to use object with state in your server, think carefully before doing it. Below you can find the code for Startup
OK, now let's build and run the solution. If all goes well, you should see something like that:
We are going to make few basic request to the server to make sure it works properly. We start Postman and make a GET request to http://localhost:3010. We have a response. As you can see shortly just few lines below, the first request to Http.Sys took some time, but after that every other request is processed very fast.
We make another request. POST to http://localhost:3010/current with some demo data:
And now, POST to http://localhost:3010/history. This call is not expected to run response data, only successfull status of 200
Let's make another one. This time the response time on server should be less than a millisecond.
And now let's get the processed word data from server - we make GET call to http://localhost:3010/history?top=100&useShow=true. The first call to a particular endpoint is fast and compared to other server products is OK. The subsequent calls to the endpoint however are as we said above, very fast. We make the exact same call:
And those are the times at server side. Compare the times for a first time call and subsequent call.
And now let's focus on what's happens after the subsequent get call, when I initiated few more of those. We are again below millisecond :)
I would advise that you spend some time and properly play with the web api, making multiple calls and testing all exposed endpoints, taking advantage of the excellent debugging support, which Visual Studio offers. When you are ready and feel OK with our solution so far, we move to the client side.
At this point of development of our solution the two desktop clients, which we have created, still reference the Business project. They use the
WordProcessor classes, defined in
this library. We have created a web server and this web server have a reference to Business library. So, it is now the web server who provides all the logic from Business, this time over the http and using application/json.
Those desktop clients no longer need a reference to Business project and they have to consume what web server provide. For that purpose we will create a project specifically for working with
HttpClient and in
this project we will also create a
Word class, which is going to be used only by clients. Then we can start modifying both Word classes according to the needs of server project and proxy project. For example,
our ranking is done always on client side, so the
Word class on server will no longer need the
Rank property. Here are the steps we are going to follow now:
Word class from Business project and we remove
Rank property. This is now the class should look after. We then compile only the business library.
We rename the default Class1.cs to Word and we rename the class name to Word as well. Then we create few properties. This is the
Word from Proxy project. If you want to change the names of
the properties, this is just fine, but then you have to add few custom lines to make the proper deserialization of the server response. And to change all those names in both clients too. And to make sure it works as
before. And..., well you get the idea.
We are going to add an enum called HttpVerbs. Here is it:
and then we create class called BusinessProxy, which will hold an instance of
HttpClient and will communicate with server. Both clients will have a reference to the Proxy project and they will
BusinessProxy class instead of
WordProcessor. We will make the method names of
BusinessProxy the same as those of
WordProcessor just to make our refactoring
of existing client code little bit easier and save some renaming.
You might be wondering - ok,
MakeRequest method returns a
Task<string>, why don't we use the Result directly on this, but we have use
Task.Run(() => something).Result. Short
answer - give it a try - it will hang out your client and you might wonder why this is hanging and there is no exception thrown. For more, you might want to consult an MSDN Magazine Article (from March 2013), where such
situations are explained in greater details - here
We build the Proxy project. Now it remains to fix the both desktop clients projects.
Then, we open the code-behind file of Main, Main.cs. It should complain about missing Business reference,
Word classes. We specify that we want to use
Proxy instead of Bussiness - see below
WordProcessor wp;we type
wp = new WordProcessorwe put
bp = new BusinessProxy()
and then, we replace every occurence of wp with bp. There should be total of 7 replacements and there should be 9 referenes to bp in total after all of our changes. Here how first changes look like:
How it is time to fix WPF project.
WordViewModeland fix the Word reference to Proxy instead of Business.
We go to
MainWindowViewModel class and make similar changes to those we did for Main.cs in WinForms project. Again, total of 9 bp occurences and total of 7 replacements, as for Winforms
At this point we are ready to build the solution and run it. All should go without troubles and HttpSys server console should appear, because this is still our starting project. This is how the final dotnet solution look like:
During debug session, we will select Solution Explorer tab, and from there, right-click on WinForms or WPF, and Debug -> Start new Instance. Or we can Publish the .NET core server project to our local drive, start it from there and run the clients the old way.
In case we want to produce an executable from a .NET Core project, we have to Publish the project. We are going to publish the project to a local folder. Those are profile settings, which we are going to use:
For this demo we have published the http.sys server as executable and we start it. As we mentioned before, first calls to every endpoint will took more time, but they still will be relatively fast, and the subsequent calls will execute very fast. So we enter some dummy text to make a history set and we put it on server. Then, every new instance of every client has this history set and start with it. We can add more words to it or update show property for every word.
If you made it so far, excellent job! We have completed part three of our article! We have created Http.Sys server for providing our existing logic over the http and we have tuned our existing desktop clients projects to use the server and to make calls to it. We are using http protocol for our communication instead some custom and so common alternative and this makes our solution even more robust. We have solved one more challange from those set at the end of chapter one - multiple users can access and use the same data, our solution is now distributed. We do not rely on database, neither on third-party service to achieve this. Using databases in production application is a must, but it introduces additonal complexity and it is risky to connect to databases over the net. Or at least not so easy as accessing urls with browsers or with fat clients. Using third-party services is common in production and they basically do everything for you, but you pay additionally for this service. And sometimes this service can be rejected by owner and your solution will no longer work in such case.
Finally, we have a web server and we have desktop clients. It remains to find a solution to last challenge from part one - easy update of client logic, for example ranking logic. One of the best candidates for that is a web client, which will provide same user functionality as existing clients, but because it is provided over the http as well, it can be very easily updated. Time for the last part, part four.
Our article has reached its final part. We are going to create a web client which will use the same endpoints, we might say the same web api as the existing desktop clients and it will provide the same functionality to the users. User experience from native application and from web application might not be the same, but in our case the interactions are simple and we can agree that in this particular case web clients can be used perfectly fine. We can continue with Http.Sys server and build more stuffs there and add more middleware, not only our custom stuffs and the middleware for responce compression. Hosting of static data can be done without any additional Nuget and it will be just fine. However, we are on the web now. We are not bound to any particular technology, we can use one or another approach. So, ASP.NET Core is good and will do the job, but let's try something else. Three part for dotnet up to now, let's make one part for node.js stuffs
Initial release of node.js was on 27 May 2009, 10 years ago from the days of writting of this article. The package manager for node.js, npm, was introduced in January 2010. Initial release of Vue.js was made in February 2014, or about 5 years ago. And initial release of axios.js was on 29 August 2014, let's say 4 and half years ago. We get an idea about the main stuffs for this chapter. It is time to move on.
Our solution should look like this:
This is the markup for Main.html. The markup of the page should be simple. We will have a textarea, three buttons and two unordered list. List template will have to display the name, count and the boolean property Show. We can control the color of the element by setting its class. The css framework which we are using colors the element if we add specific word to its class. We are going to use this. For filling the data we are going to use Vue.js provided binding. The idea is very, very similar to the situation that we have with the WPF client. This similarity will be further emphasized when we look at the main.js script few lines later.
The main.js will contain the code for the web client. Axios will be used for making async calls to the server and vue.js will do the binding, event handling, property change tracking etc. We have called the main Vue
mainViewModel to again emphasis simularities between MVVM approach we used in WPF client and approach here. Those framework models are called MV* for a reason. However, let's give more details on
And finally, here is the main.js file. If you have some troubles here, use a browser debugging IDE and put console.log or breakpoints. Not so cool as native debugggin in Visual Studio, but it is not so bad either. Getting better with time.
Now it is time for final part of node.js solution - the server file. Node.js will serve two functions - will host the main.html (we can open the file main.html manually and it will still work, if node.js is running and api part is accessible) and provide it with css and js files, and will proxy request from /api to Http.Sys server. For making the proxy calls we will use the http-proxy-middleware. Pecularity of this middleware is that if node.js uses npm package body-parser or in newer versions (as ours :)) even express.json(), this means that node.js will read the buffer of a POST or PUT or other request*. And buffer usually can be read only once. Why - outside the scope of this article. So, in such case we have to re-write the buffer and move on. This is why there is commented code on the screen-shot.
And that's it for the web part. We are ready with web client stuffs. Time for the final live demo!
Cool! We have achieved the remaining goal from part one - easy deployment of a client. It's so easy like typing the url and confirming. Now we have both desktop and web clients and we have native experience and if needed, very easy deployment. The idea is that all clients should be used together. We did a good job!
Still, now we have to care for two web servers using two different languages, clients developed using one framework and language, and other clients developer using another language and other frameworks. Surely there will be new challanges as well.
Maybe it is overly simplistics, but I hope it emphasis at all points on which I intended. I have tried to make it like a retrospective of how the development was years ago, what changed then, and what changed after that. Of course, there are some things which were missed, some things were not considered in great details, but it is what it is. I have checked typos (code or functional) and I hope there are none which are considerable. I hope someone find this article interesting.