The UI - It Does Nothing!
The UI should have no logic
That's not strong enough.
THE UI MUST DO NOTHING!
That's better.
The UI is the edge of our system. The edges of our system are hard to test. They interact with things that are not ours. We don't control them and we can't ensure the conditions the encounter. We can't always test it.
To minimize the risk to our system we exclude behavior from the boundary of our system; the Operating System and 3rd Party Libs.
The other one is the User.
The User post is focused on the UI controls themselves, how to interact with the user.
What is the UI?
The UI is how our app interfaces with the user... User Interface.
The "code behind", to borrow ASP.NET web terminology, is only there to glue the UI to the application behavior.
In this post what I mean by UI is the "container" that the OS interacts with to display something from your App. In Android, it's the Activity (ignoring Fragments, but they would fall under this as well). For a UWP app, the Page. I don't recall what iOS has. WinDesktop Apps, it's the Form class. A console app gets more complicated because we kinda have to create our own "code behind" - so it'll be left out of this example.
What does the UI do?
I said very clearly at the beginning, it must do nothing.
That's not QUITE true. There are a few things it does for us.
- It receives ui events.
- It invokes "event action" objects.
- It returns interfaces.
This is it. The UI does three things.
This is a new topic for many people. These definitely need some more explanation and discussion; so let's get to it!
Receives UI Events
Our UI is going to receive some form of input from the system. These events are outside of our control. The UI code behind base class hooks into the OS. This OS integration makes it impossible to instantiate our class. Using tools that negatively impact the code, tests, and developer discipline can certainly allow us to instantiate it. This is a negative impact which isn't needed at all when we can simplify what the UI does to the point of there being no logic or behavior to actually test.
This is exactly where our event receivers are going to be hooked up.
Such as a UWP button click for a SubscribeButton
private void AlignmentMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
// What goes here?
}
This is part of the UI - clearly the XAML is UI. The Page is where the event comes into the code; it's a bookend of our system. This applies to any event from the user interface. I'll focus on the simple one of a MenuFlyoutItem. I'm using the docs example of a MenuFlyoutItem button (even though I hate thier naming convention).
What should be do then when this is clicked?
The referenced docs have the full method as
private void AlignmentMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
var option = ((MenuFlyoutItem)sender).Tag.ToString();
Windows.UI.Text.ITextSelection selectedText = editor.Document.Selection;
if (selectedText != null)
{
// Apply the alignment to the selected paragraphs.
var paragraphFormatting = selectedText.ParagraphFormat;
if (option == "left")
{
paragraphFormatting.Alignment = Windows.UI.Text.ParagraphAlignment.Left;
}
else if (option == "center")
{
paragraphFormatting.Alignment = Windows.UI.Text.ParagraphAlignment.Center;
}
else if (option == "right")
{
paragraphFormatting.Alignment = Windows.UI.Text.ParagraphAlignment.Right;
}
}
}
I love this example because it plays into a few things I do that will show some UI interactions.
We'll need to reference the XAML later on, so it gets to appear here as it is in the documentation.
<DropDownButton ToolTipService.ToolTip="Alignment">
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
<DropDownButton.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuFlyoutItem Text="Left" Icon="AlignLeft" Tag="left" Click="AlignmentMenuFlyoutItem_Click"/>
<MenuFlyoutItem Text="Center" Icon="AlignCenter" Tag="center" Click="AlignmentMenuFlyoutItem_Click"/>
<MenuFlyoutItem Text="Right" Icon="AlignRight" Tag="right" Click="AlignmentMenuFlyoutItem_Click"/>
</MenuFlyout>
</DropDownButton.Flyout>
</DropDownButton>
This is a blog post, I don't have the bandwidth to do this exactly like I would with all the supporting infrastructure; we're looking at how to get the UI to do the minimal is should.
I simplify a bit; mostly around Primitive Obsession. It should make the practice around UI's simpler to process. :)
How can we test this method? We can't instantiate the Page... this is untestable code. I reject the idea we can have untestable code. A lot of my discovery of techniques, especially here, came from refusing to accept "This is UI, it's hard to test".
We receive this event, and need to perform this behavior... How?... What do we do?
Invokes "event action" objects
What we do is invoke an object that represents the behavior the user wants to perform.
In this case we want to adjust the alignment - Wonderful. Let's have an object do that for us.
public sealed partial class MainPage : Page, IMainPage
{
private readonly IAdjustAlignment _adjustAlignment;
public MainPage():this(new AdjustAlignment()){}
private MainPage(IAdjustAlignment adjustAlignment)
{
_adjustAlignment = adjustAlignment;
// Custom initialization excluded
this.InitializeComponent(); // This is why we can't test the class
}
private void AlignmentMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
_adjustAlignment.Adjust(new FrameworkElementTagReader((FrameworkElement)sender), this);
}
}
There's a couple things here. First is my use of double constructors for manual dependency injection in the constructors
public MainPage():this(new AdjustAlignment()){}
private MainPage(ITagReader tagReader, IAdjustAlignment adjustAlignment)
{
_adjustAlignment = adjustAlignment;
...
}
The next is that MainPage
implements IMainPage
.
public sealed partial class MainPage : Page, IMainPage
Having an interface we control on the Page is important for this technique for the same reasons we want to Interface Everything. ... Apparently I still need to write that post... Oops... Anyway, interface everything. OK; good.
We'll use the interface to access the information shown in the documentation method.
The next is the FrameworkElementTagReader
. This is how our UI returns just interfaces. Everything that leaves our UI code behind is wrapped or implementing an interface we control.
In this case we're wanting access to the Tag
property of our sender. We can provide that through the lowest heirarchy element the Tag property exists on.
Now, we're getting into how the UI needs to return UIs. It's how we write behavior around UI elements AND have them 100% testable.
Returns Interfaces
We interface everything to ensure we abide by known contracts, which expose only behaviors. Our UI will only return interfaces. It won't give back a TextBox
. We want Clean Architecture like I tried on an early version of these technical practices in an android app.
We can't give lower levels of our code a UI element. UI is the outer most level, nothing internal knows about UI elements.
Since we can't return the controls, we return an interface. We can do this through Interface Overloading but that doesn't always work. I think it will always for Android; but flat out fails for UWP. My UWP project at work forced a change, which I like better.
Let's look at how we'd implement the FrameworkElementTagReader
before we start digging into the behavior the method previously contained.
I'll just drop the code in, then we'll discuss it.
public sealed class FrameworkElementTagReader : ITagReader{
private readonly FrameworkElement _element;
public FrameworkElementTagReader(FrameworkElement element) => _element = element;
// This is the (only) method defind by ITagReader;
public string Tag() => _element.Tag;
}
We create an interface for the behavior we want; to read the tag; ITagReader
.
This class simply holds a FrameworkElement
and provides access to it's tag. We don't have to know WHAT type it is; just the bases type. The interface allows us to fake really easy.
There's something I don't think we'd ever need; but what if our sender isn't a FrameworkElement
, what if it's a UIElement
? I don't know if that's possible from the UI; but our events get an object. The forced casting can fail in such a way that it returns null
; our nemesis. Having a single class to provide tags gives us a nice easy way to handle this; and only in one place.
public sealed class FrameworkElementTagReader : ITagReader{
public string Tag() => _element?.Tag ?? string.Empty;
}
I'd do something different using FluentTypes repo, but it serves well enough for this example.
Let's get to the behavior we want to modify...
The first thing the code does is check a null...
Windows.UI.Text.ITextSelection selectedText = editor.Document.Selection;
if (selectedText != null)
Well; We don't use null's. We much prefer the null object. While this isn't the intent, we'll show it a little.
The code that the original is being morphed into follows the microObjects style very closely, with some details left out. The big one is that I employ a chain of responsibility for decision making.
The null check is hiding something. It's complicating the functionality through poor representation.
null
hides something
We don't care if the selectedText
is null
; we want to know if there IS selected text.
Since we're going to use a null object pattern, we don't need to implement the null check... Neat. Code is simplified! We can just compare and our NullObject will behave properly and never be equal.
The next thing we want to do is compare behavior the var option
to various values. Perfect, I love comparing things!
public sealed class LeftAlignment : IAdjustAlignment{
public LeftAlignment(IAdjustAlignment next)=> _next = next;
public void Adjust(ITagReader tagReader, IMainPage mainPage){
ApplyAdjustment(tagReader, mainPage);
_next.Adjust(tagReader, mainPage);
}
private void ApplyAdjustment(ITagReader tagReader, IMainPage mainPage){
if(!string.Equals("left", tagReader.Tag()) return;
mainPage.SelectedTextAlignmentWriter().Alignment(ParagraphAlignment.Left);
}
}
This isn't what I'd actually do. Sorry - This still hurts me. How simply and direct this class is... it's not MicroObject enmough for me. It doesn't take the Technical Practices as far as we can.
Let's take a slight detour, look at how I'd actually do this; and then we might return to the simplified form. ... I doubt it; it hurts me too much.
MicroObject'd
public sealed class LeftAlignment : IAdjustAlignment{
private readonly static Alignment _alignment = Alignment.Left;
public LeftAlignment(IAdjustAlignment next)=> _next = next;
public void Adjust(ITagReader tagReader, IMainPage mainPage){
ApplyAdjustment(tagReader, mainPage);
_next.Adjust(tagReader, mainPage);
}
private void ApplyAdjustment(ITagReader tagReader, IMainPage mainPage){
if(mainPage.AlignmentLeftTagCompare().IsNotEqual(tagReader)) return;
mainPage.SelectedTextAlignmentWriter().Write(_alignment);
}
}
There we go.
That feels better.
We're not using the UI alignment property, we've got ourselves an object representation we can use. I'm hiding the callback nature of what the Alignment
class looks like. I keep this at the top instead of inline as static things are easier to modify and know about when we isolate them at the top there.
Instead of comparing to a string value contained in the class; let's actually get the object with the tag we want to know it matches and compare against it. Then if we ever tweak the value of the tag it's working everywhere still. Any values like the tag should only ever exist in a single location. When it's in multiple places; it implies a representation is missing.
OK, we've got that class... but there's a lot going on that we haven't implemented.
Let's address AlignmentLeftTagCompare
first.
This method returns something, which I shall call ITagCompare
. This allows us to compare our clicked tag to our Left Alignment object's tag.
public sealed partial class MainPage : Page, IMainPage
{
private ITagCompare AlignmentLeftTagCompare() => return new FrameworkElementTagCompare(LeftAlignmentMenuFlyoutItem);
}
Note: Our XAML has to be updated with x:Name
value, which I'm not showing here beyond this: <MenuFlyoutItem x:Name="LeftAlignmentMenuFlyoutItem" Text="Left" Icon="AlignLeft" Tag="left" Click="AlignmentMenuFlyoutItem_Click"/>
The FrameworkElementTagCompare
is going to look A LOT like our FrameworkElementTagReader
.
public sealed class FrameworkElementTagCompare : ITagCompare{
private readonly FrameworkElement _element;
public FrameworkElementTagCompare(FrameworkElement element) => _element = element;
// This is the methods defind by ITagCompare;
public string IsEqual(ITagReader tagReader) => string.Equals(_element?.Tag,
tagReader.Value());
public string IsNotEqual(ITagReader tagReader) => !IsEqual(tagReader);
}
This just compares tags. It doesn't need to, nay - SHOULDN'T - know how to do anything else.
The next method we have is the SelectedTextAlignmentWriter
.
public sealed partial class MainPage : Page, IMainPage
{
private ISelectedText SelectedTextAlignmentWriter() => return new RichTextSelectedTextAlignment(editor);
}
public sealed class RichTextSelectedTextAlignment : IAlignmentWriter{
private readonly RichText _element;
public FrameworkElementTagReader(RichText element) => _element = element;
// This is the (only) method defind by IAlignmentWriter;
public void Write(Alignment alignment) => _element.Document.Selection?.Alignment = alignment.Value();
}
RichTextSelectedTextAlignment
is a bit busy. I'm doing all the coding in Ghost as I'm typing the blog... so it's just CLOSE to what I'd do. Not sure how I'd break that one down further right now. I don't like it; lots of opportunity to break; which should be handled in classes.
If we look at the three UI wrapping classes, RichTextSelectedTextAlignment
, FrameworkElementTagCompare
, and FrameworkElementTagReader
we can see some amazing similarities.
That's the style of all my wrappers. They do one thing. There's typically two forms, Reader
and Writer
. Compare is usually a behavior based on whatever the Reader
returns. I'm playing around with a Compare
to see how it flows.
Continuing on
Our CenterAlignment
and RightAlignment
classes will look very much like our LeftAlignment
. These need to be bundled together somehow in our _adjustAlignment
class.
This is a missing post; "Chain of Responsibility". The way I bundle these is to represent the behavior I want in an object that knows what can do the appropriate behaviors.
public sealed class AdjustAlignment : IAdjustAlignment{
private readonly IAdjustAlignment _next;
public AdjustAlignment() : this(new LeftAlignment(new CenterAlignment(new RightAlignment(new AdjustAlignment.NoOp())))){}
public AdjustAlignment(IAdjustAlignment next) => _next = next;
public void Adjust(ITagReader tagReader, IMainPage mainPage) => _next.Adjust(tagReader, mainPage);
public sealed class NoOp : IAdjustAlignment{
public void Adjust(ITagReader tagReader, IMainPage mainPage){}
}
}
The UI Does Nothing
All these practices come together to enable us to have a UI that does nothing. That code has no logic. It has no behavior to test. It's langauge and system that we'd be testing - Which we don't need to test.
The untestable has been made untestable.
Wha... hehe - We had logic in our UI that we couldn't test. Behavior and functionality wasn't within our Unit Test capability - It was untestable.
We covered techniques that when applied force the UI into a state where what it does is constrained to things provided by the langauge and system that runs our code. These are things that we can not, should not, and do not, test - it's untestable.
The value in the second set of untestable is that we don't have any logic or functionality outside of our test capability. We can test everything we should, and want, to be able to test.
Out UI does nothing; this is beautiful.
Summary
Through this far too long blog post; I've touched on the techniques I use to extract all logic from the UI.
How I can have the UI perform the barest of functionality to just provide interfaces to the rest of the application for behaviors.
I touched on a lot of other practices that all deserve their own blog posts to get into the indepth explainations of the how and why... I have those as things to write.
Hit me up on twitter if you're interested in a specific one. Someone asking tends to get them written slightly faster. :)