I've written about my approaches to the UI before. The one that shows an example is The UI Does NOTHING!. It takes a UWP example and transforms it into what I'd do.

I haven't really done a more exhaustive look at this process, or implemented it myself; So let's revisit. it. I think this is a topic that'll need some repeat posts. It's a very different approach to the UI layer than I've seen elsewhere. It's a technique I've found very useful everytime I've applied it.

The basic premise here is authentication. I'm going to "log in" through the UI. First version will be a WinForms app.

The end state will be MicroObjects following the technical practices. We shouldn't start there though. Don't try to predict the objects you'll need.

You'll be wrong.
I guarantee it.

Some interaction, some relationship; something will be missed or anticipated incorrectly. This becomes a REALLY hard 'sunk-cost' situation to get out of. Don't get into it. Write everything imperative.

Behavior

The first thing we want to do is get the desired behavior implemented. When we focus on a SMALL task. A thin, vertical slice - It's easy to implement the whole of the desired behavior in an imperative way. We don't need a lot of structure or patterns in place for a correctly sized slice of work. If you need to do more than a single (large) method can handle - Your chunk of work is too big.

As you develop this way, and produce more and more MicroObjects, the creation of the desired behavior happens faster and faster. We can start to deliver new complex functionality quickly. Simply by combining our simple functionality.

What I really enjoy about this style of developing functionaltiy is we end up with ONLY what we NEED. There's no gold plating or future proofing. Do what creates the required functionality, nothing more. When you add more functionality, repeat, then refactor.
I find that doing this consistently prevents need for much big effort refactorings.

We can see in this commit where I got the basics of the functionality in place that there are no objects. It's all imperative. It's all in the UI layer.

private void btnLogIn_Click(object sender, EventArgs e)
{
    //If valid
    if (txtUserName.Text == "Quinn")
    {
        //  hide Controls
        lblUserName.Hide();
        txtUserName.Hide();
        lblPassword.Hide();
        txtPassword.Hide();
        lblError.Hide();
        btnLogIn.Hide();

        //  show controls
        lblWelcome.Show();

        //  set values
        lblWelcome.Text = "Welcome Quinn";
    }
    else
    {
        //  Show error
        lblError.Show();
        txtPassword.Text = string.Empty;
    }
}

This is how I start. Everytime I try to jump to objects sooner, it makes the problem harder. Always.

Objectification

The next step is to identify behaviors. We can kinda see some of it by the comments that are in the code. Or the whitespace creating the logical groupings. Those are great places to start... I go a little different route.

Every action is a behavior. These behaviors are concepts in our code. We want to represent the concepts that exist in our code as objects in our code.
That's the route I go. If I can isolate and name it; it's deserving of an object.

The results of my initial efforts to extract MicroObjects are in this commit. It would have been great to do it a step at a time... I got distracted and just kinda cranked through.

The button click method now looks like

private void btnLogIn_Click(object sender, EventArgs e)
{
    IMainForm mainForm = this;
    //If valid
    if (txtUserName.Text == "Quinn")
    {
        //  hide Controls
        mainForm.LogInControlsVisibility().Hide();

        //  show controls
        mainForm.WelcomeLabelVisibility().Show();

        //  set values
        mainForm.WelcomeLabelWriter().Write("Welcome Quinn");
    }
    else
    {
        //  Show error
        mainForm.ErrorLabelVisibility().Show();

        //Clear password
        mainForm.PasswordTextBoxWriter().Write(string.Empty);
    }
}

The concepts represented by comments (minus the //If valid) are now represented as Objects in the code. We've given them names. We've reduced the amount we have to look at and now have a simpler interface and functionality than we did before.

One of the really important things that comes from this is how related behaviors can be grouped into a single behavior for the rest of the system. Encapsulate simple behaviors into a new object to enable simple use of complex behaviors.

OK, hiding a bunch of controls isn't a particularly complex behavior... I had to adjust though. This is a .NET Core app (3.0) which apparently doesn't have a Frame Ui element. I can't put all of my login controls into another element (ok, I probably just don't know which one to use) so I have to hide each one individually.
I mimic the same functionality of having that container by encapsulating all of the control hiding inside a single class. I've represented the concept of "Login Controls Visibility" as an object LogInControlsVisibility.

internal sealed class LogInControlsVisibility : MultipleControlsVisibility
{
    private readonly IMainForm _mainForm;

    public LogInControlsVisibility(IMainForm mainForm) => _mainForm = mainForm;

    protected override IEnumerable<IVisibility> Controls()
    {
        return new List<IVisibility>
        {
            _mainForm.UserNameLabelVisibility(),
            _mainForm.UserNameTextBoxVisibility(),
            _mainForm.PasswordLabelVisibility(),
            _mainForm.PasswordTextBoxVisibility(),
            _mainForm.ErrorLabelVisibility(),
            _mainForm.LogInButtonVisibility()
        };
    }
}

My UI is simplistic, so the "Welcome Label" doesn't have this same type of representation. It would if it was more advanced; but I only do what's needed. I only want the constructs and abstractions in place that I /need/ at the moment.

UI Specific

We're at the UI layer, SOMETHING must know about the UI controls. I consider them untestable as they need the system to actually function. We need to interact with them in SOME way, and the way we do that in a testable fashion is with abstractions. In this situation, and most that I've encountered, I'm using an interface.
The most prolific tends to be centered around Visibility, Read, and Write functionality. I know others have existed, but since I don't recall them, clearly not heavly used. :)

I always aim for the highest level in the hierarchy that owns the functionality I need. This relies on the UI heirarchy not doing bad things. In C# using the new operator on a method/property being used will often produce unreliable results because I'm interacting with the base class functionality. Not good.

Visibility is owned by the Control class. For Visibility I'll then create a ControlVisibility to implement an IVisibility interface.

public interface IVisibility
{
    void Show();
    void Hide();
}

public sealed class ControlVisibility : IVisibility
{
    private readonly Control _control;

    public ControlVisibility(Control control) => _control = control;

    public void Show() => _control.Show();
    public void Hide() => _control.Hide();
}

I now have a single implementation to control the visibility for... 99%? of the controls I'll be interacting with.

When creating the UI functionality classes I like to use the format [CONTROL_TYPE][INTERFACE]. It provides a high level of information when reading the code later.

The ControlWriteText is very similar. Text starts on the Control class, allowing us to have a ControlWriteText : IWriteText setup.

public interface IWriteText {
    void Write(string text);
}

internal sealed class ControlWriteText : IWriteText
{
    private readonly Control _control;

    public ControlWriteText(Control control) => _control = control;

    public void Write(string text) => _control.Text = text;
}

So far, super simple. They do a single thing. I realize I could break apart the visibility into IShow and IHide, but it hasn't shown itself as valuable a breakdown... yet. Now that I think about it... I kinda like that. I have a third method that I removed from the snippets above that is in the repo, ChangeTo(Visible visible). Which allows specifying what visibility to have w/o knowing in advance which visibility it should be. Could be bool as the param; I don't actually use it in this example; but also havne't refactored it out. ... I wrote code w/o requiring it... I guessed wrong... Oops.

Anyway, that would create a third interface. This REALLY drives the 'Interface Segregation Principle' to the extreme... which I like. It really helps clarify the intent.
I'd like to refactor the UWP app with this idea and see how it changes. ... Another day. :)

I'll be leaving it as a combined interface for now. ... Especially since it's already in the git history...

Bound by Chains

I use the Chain of Responsibility pattern A LOT. It's definitely my hammer. I'm glad to recognize this as it helps me question the use... and then I still use it. It's my sledgehammer. Anytime I need a sequence of behaviors, I use it to have each class as a behavior, and then coordinate.
The pattern should be a post of it's own... Might be... IT IS! Neat.

The next step was to ... ... I just did a bunch of stuff, created my CoRs, added some hard coded authentication. For the purposes of blogging; definitely would have been nice to have committed in smaller steps.
I'll leave this as an opportunity for the reader to explore the code... no? Fine... It does stuff.

This is where the all of the functionality got finished. Validating the username and password. Changing the username/password to be classes representing the concepts that exxist. When we do this we get to control and limit exposure of sensitive things. The Password, in this case, is never exposed. It's never given out. You can give it something to compare to, but never see the source.
While, sure, it's our current memory space on our computer - We can dig into it. A server; if we have the raw value, MAYBE we screw up and expose it. If we can NEVER get the data, we can never accidently expose it. This is a huge reason the practice of "No Getters/No Setters" is such a massive reduction in complexity. We don't have to protect things we can never get.

Once I hit the functionality and chained things up; the code looks like this.

The next step was to extract the non-UI specific code into a seperate project.

From there, I implement a Console app for the functionality. It's not perfect. Far from it. What I enjoy is that this is the second 'feature'. It highlights a lot of assumptions or "Winform" centric thinking/behavior. OK, not a lot, but it's obvious.

The next step will be to refactor it into appropriate interfaces/places. Clean it up. But! - It works. The CORE of the system is untouched and implmented as a winform and console app. The UI is the only variance and each has custom implementations for their specifics. Perfect; no. Working; yes.

Finished

This is a contrived example, so I wasn't too keen on making everything perfect before blogging. It's a work in progress. I want to do a REST api style version as well. These all behave differently and may start to evolve the API. A REST service has to return something, which neither the WinForm or Console have to do... so... It's gotta change somehow? Maybe? I don't know.

It's FANTASTIC not knowing the answer. It's a discovery waiting.