DEV Community

Cover image for S.O.L.I.D Windows Forms
Nick Proud
Nick Proud

Posted on

S.O.L.I.D Windows Forms

We all know that Windows Forms is old. Like really old. (Especially so in software terms). Windows Forms shipped with the version 1.0 of the .NET framework back on 13th February 2002, long before I had committed myself to a life of coding.

Windows Forms may have already been a seasoned UI framework when I started my programming career but like a lot of developers starting out with .NET (or even VB6), the Windows Form editor is where you get your first taste after playing with the console.

I always thought they would be dropped quickly. Soon I would be mostly developing front-ends for desktop applications using XAML, or even exploiting the benefits of Electron to deploy web applications as native desktop applications.

But no. I still find myself confronted with requirements to support Windows Forms apps and you know what?

That's fine.

But does that mean we have to structure our code in a archiac way? Of course not!

The last decade has seen the proliferation of a fantastic new movement in software engineering practice in the Clean Code Movement.

Clean code pioneers like Robert C. (Uncle Bob) Martin have paved the way for easier to read, manageable and maintainable code, etching in stone the new holy commandments to which all developers should take note:

The S.O.L.I.D. Principles

Most developers these days have come across some mention of these new principles but in case you need a solid (meheh) definition check out the wikipedia entry for them. There are many easier to digest explanations in book form. I recommend Robert C. Martin's classic Clean Code: A Handbook of Agile Software Craftsmanship

Yeah but who cares about SOLID in Windows Forms?!

I like to think that I adhere to these principles as much as possible. It's not easy but nothing worth doing is at first. However, when it comes to developing code for UI, I have to admit that can sometimes get a little lazy.

I'm more of a 'back-end' developer. I like building the engine. Of course, I want everything to look nice and be easy to use, but I don't get as excited about front-end development. This started to show when I was recently developing the front-end for my Windows Event Log automation tool, AutoEvent, which just so happens to use Windows Forms. (Yes I still use Windows Forms from time to time. I prefer web front-ends but sometimes Windows Forms is all you need!)

I was writing code to manage the way the main page renders UI controls and making different controls visible/hidden when a particular tab was clicked. A basic toggle.

It's common to have to add new functionality on top of small blocks of code as you progress through your UI development in a basic app but I found myself writing some sloppy toggle logic. Take the below button event for example, which changes the UI elements that are visible based on whether I click the *'*Settings' or 'Triggers' tab.

 private void RibbonControlAdv1_SelectedTabItemChanged(object sender, SelectedTabChangedEventArgs e)
        {
            switch(ribbonControlAdv1.SelectedTab.Text)
            {
                case "Settings":
                    panelSettings.Visible = true;
                    panel1.Visible = false;
                    lblMailServerConfig.Visible = true;
                    lblProcessLog.Visible = true;
                    lblSMTPPassword.Visible = true;
                    lblSMTPPort.Visible = true;
                    lblSMTPUsername.Visible = true;
                    lblProcessLog.Visible = true;
                    txtProcessLog.Visible = true;
                    textSMTPPassword.Visible = true;
                    textSMTPPort.Visible = true;
                    textSMTPServerName.Visible = true;
                    textSMTPUsername.Visible = true;
                    break;
                case "Triggers":
                    panel1.Visible = true;
                    panelSettings.Visible = false;
                    lblMailServerConfig.Visible = false;
                    lblProcessLog.Visible = false;
                    lblSMTPPassword.Visible = false;
                    lblSMTPPort.Visible = false;
                    lblSMTPUsername.Visible = false;
                    lblProcessLog.Visible = false;
                    txtProcessLog.Visible = false;
                    textSMTPPassword.Visible = false;
                    textSMTPPort.Visible = false;
                    textSMTPServerName.Visible = false;
                    textSMTPUsername.Visible = false;
                    break;
                default:
                    break;
            }
        }

Depending on your viewpoint, it might be obvious that this is dirty. Or you might have little to gripe about it. It works right?

Well yeah, it's...functional but it's not very maintainable. For example, the the event started with the management of just a view controls, but I kept adding more. Soon I had to manage a new tab which in itself had more and more controls to show when it is clicked.

I'm not even done with the number of tabs I need to add at this point. The potential result is an ugly, long switch statement.

But how does this break SOLID?

S is for 'Single Responsibility'

Does this code 'do one thing?' Well I guess it does, but not obviously. In fact, it does several things that should be encapsulated. It checks through lots of different potential button clicks and then sets control visibility values one by one, in the same event.

Doesn't is seem to be a lot of 'stuff' all crammed into a single UI event? How can we clean it up?

Hello Abstraction! How I love you so.

public abstract class MainFormLayout
    {
        protected frmMain FormInstance;

        public MainFormLayout(ref frmMain formInstance)
        {
            this.FormInstance = formInstance;
        }

        public abstract void Activate();
    }

I created an abstract base class called MainFormLayout. This class will serve as the template for a set of derived classes which take in a form (in our case the main form we want to toggle controls on) and then allow us to call a single Activate() method which simply turns on all the correct UI controls based on the navigational tab you clicked. So if you clicked Triggers, instead of looping through a dirty, clumsy set of switch cases in the very event that the button-click fires, it just grabs an instance of the TriggersLayout class, derived from our base class and takes care of the control rendering.

 public class TriggersLayout : MainFormLayout
    {

        public TriggersLayout(ref frmMain formInstance):base(ref formInstance)
        {

        }
        //here is our Activate method, overriden from base class.
        public override void Activate()
        {
            //We can make Activate() call some methods as needed.
            DisableSettingsPanel();
            EnableTriggerPanel();
        }

        private void EnableTriggerPanel()
        {
            ((Panel)(FormInstance.Controls["panel1"])).Visible = true;
            foreach(Control control in FormInstance.Controls["panel1"].Controls)
            {
                control.Visible = true;
            }
        }

        private void DisableSettingsPanel()
        {
            var settingsPanel = ((Panel)(FormInstance.Controls["panelSettings"]));
            if(settingsPanel != null) settingsPanel.Visible = false;
        }

    }

This class already knows which controls to pick from the requested layout, and with a single method call, it performs the necessary iterations over the controls we need and makes them visible, but how do we get the class?

Open for Extension only! (Thanks to the Factory)

We create a static class using the Factory Pattern. A factory class does a simpler version of what the logic was doing at the top level of abstraction. It checks which layout you have asked for and then sends it back. It's a little more functional in that you send a request and it just gives you an answer; in this case, the answer being the layout you require. Nothing more, nothing less. In this example, I have created a MainFormUIController. It may be referred to as a controller but it is still really just a factory class as it fetches and activate the desired configuration.

 public static class MainFormUIController
    {
        public static void SwitchLayout(frmMain form, string layout)
        {
            var thisLayout = GetLayout(ref form, layout);
            thisLayout.Activate();
        }

        private static MainFormLayout GetLayout(ref frmMain form, string layout)
        {
            switch(layout.ToLower())
            {
                case "triggers":
                    return new TriggersLayout(ref form);
                case "settings":
                    return new SettingsLayout(ref form);
            }

            return new UnknownLayout(ref form);
        }

    }

This methodology is more conducive to the Open/Closed principle of SOLID, because we do not have to modify any underlying logic to add more layouts. We can simply make a new derived layout class and add it to the switch list. Yes, we had a switch list to begin with, but can you imagine how big the list would get in the event!? If you're not convinced, look at the event's new code since we created out factory and layout classes:

 private void RibbonControlAdv1_SelectedTabItemChanged(object sender, SelectedTabChangedEventArgs e)
        {
                MainFormUIController.SwitchLayout(this,  ribbonControlAdv1.SelectedTab.Text);
        }

The logic for switching controls on a tab click in now one line. ONE LINE.

What's more, we are satisfying another chunk of the SOLID paradigm: Dependency Inversion.

This event no longer depends on directly knowing anything about how the UI controls are found, which controls are needed, where they are arranged etc. It has one dependency going down and the underlying code has minimal dependancies coming from the highest abstraction layer in the form of a reference to the main form and the name of a layout.

We can even eliminate a dependency on null checking. For example, what if we send the factory a layout it doesn't recognize? Do we return null?

Please no. Stop returning null.

Instead, I return a layout which is designed to catch the error and throw an exception.

    public class UnknownLayout : MainFormLayout
    {
        public UnknownLayout(ref frmMain formInstance):base(ref formInstance)
        {

        }

        public override void Activate()
        {
            throw new Exception("Requested from layout was not recognised");
        }
    }

When the factory tries to call Activate() an automatic exception is thrown with a clear error message which can be shown in a Messagebox. I'm still not sure whether this 'null avoiding' design pattern fits neatly into one of the SOLID areas so if anyone can enlighten me, feel free to get in touch.

So there you have it. Windows Forms may be old but it is just as deserving of SOLID design practices as sexy UI frameworks like Angular or Vue. You may have to use this old-timer of a platform yet, and why not? It still allows Enterprises to build extremely powerful applications quickly, saving time and money. Microsoft as even confirmed that Windows Forms will not only be open sourced, it will be supported in .NET 5.

So until someone sadly puts Windows Forms out to pasture, (which could be a while), keep em' SOLID.

Top comments (0)