This is the main interface that all windows, and objects that wrap a window, or that wrap the System.Console
writer. It implements the almost everything that System.Console
does with some extra magic.
IConsole
is a well thought out .NET System.Console abstractions. Use to remove a direct dependancy on System.Console and replace with a dependancy on a well used and well known console interface, IConsole
, to allow for building rich 'testable', high quality interactive console applications and utilities.
I have put extra care into the design of the interfaces so that developers can choose the smallest interface that meets their needs in order to enable interoperability between open source console library and app developers.
If you have any questions about how to use these abstractions (interfaces
), please join the discussion on our gitter group at : https://gitter.im/goblinfactory-konsole/community or contact me directly and I will be glad to help.
Yup, and many people do. A quick search on Github returns more than 21K+ projects with their own form of IConsole or IConsoleWriter, so its very common (and easy) to do exactly that.
However, It's not about writing a wrapper, that is very easy. It's about setting a standard of interoperability between everyone that uses this as their interface. It's also about saving time. Since you would have started by writing your own interface, and then also writing something that implements that interface, why not save yourself the 2 or 3 hours you will be sidetracked doing that and dive right in to cleaning up your code.
You can use IConsole
as simply as typing, add package Goblinfactory.Konsole
. You can always come back later and remove it.
This is the sum of all interfaces. It will require the most work to implement. Typically you often only need IWrite
and-or IPrintAt
or IPrintAtColor
.
public interface IConsole : IPrintAtColor, IConsoleState, IWriteColor, IScrollingWindow
{
}
If the app you are refactoring does not set the cursor position, and merely "writes" out via System.Console
then use the IWrite
interface as your dependancy. IWrite is good enough for 99% of System.Console
refactorings, where you're essentially just logging stuff to the console.
Pick the narrowest set of features that the class that you are refactoring depends on.
Logging, printing only? IWrite
, Needs to print in color? IWriteColor
, Need to change the cursor position when printing? IPrintAt
, eed to scroll portions of the screen? IScrolling
, Need all of the above? IConsole
.
Need only 2, e.g. printing only (no color) and printing at? Then use interface inheritance and implement just the bits you need. For example.
public interface IPrint : IWrite, IPrintAt { }
public class MyClass {
public MyClass(IPrint print) { ...}
...
_print.PrintAt(0, 60, $"Total {total}");
}
Typically use for Logging and printing only. Nothing fancy, just writing something out the console or the build output.
public interface IWrite
{
void WriteLine(string format, params object[] args);
void WriteLine(string text);
void Write(string format, params object[] args);
void Write(string text);
void Clear();
}
Change the foreground and background color of what will get printed with the next Write, or WriteLine command.
public interface ISetColors
{
ConsoleColor ForegroundColor { get; set; }
ConsoleColor BackgroundColor { get; set; }
/// <summary>
/// Set the foreground and background color in a single threadsafe way. i.e. locks using a static locker before setting the individual ForegroundColor and BackgroundColor properties.
/// </summary>
/// <remarks>Setting Colors = new Colors(Red, White) must be implemented such that it is the same as having called { ForegroundColor = Red; BackgroundColor = White }</remarks>
Colors Colors { get; set; }
}
The eagle eyed amongst you will have spotted the single class file in this contract. Being able to specify the colors for something with a single assignment makes a lot of code easier to read.
myFoo.ForeGroundColor = System.ConsoleColor.Red;
myFoo.BackgroundColor = System.ConsoleColor.White;
vs
myFoo.Colors = MyStaticThemes.Default;
also, if you're writing Threadsafe code, then you'll be saving and restoring colors a lot.
lock(_staticLocker)
{
try
{
var currentColors = myFoo.Colors;
myFoo.Colors = MyTheme.Highlighted;
myFoo.WriteLine("I am highlighted item");
}
finally
{
myFoo.Colors = currentColors;
}
}
}
In fact, the pattern above is such a common pattern that the interface IConsoleState
includes a dedicated method just for your implementation of that threadsafe pattern void DoCommand(IConsole console, Action action);
the above code then becomes
myFoo.DoCommand(()=> {
MyFoo.Colors = MyTheme.Highlited;
myFoo.WriteLine("I am highlighted");
);
Set the foreground and background color in a single threadsafe way. i.e. locks using a static locker before setting the individual ForegroundColor and BackgroundColor properties. Colors is a syntactic shortcut for getting or setting both the Foreground and Background color in a single assignment. For example
calling
console.Colors = new Colors(Red, White);
must be implemented such that it is the same as having called
console.ForegroundColor = Red;
console.BackgroundColor = White;
If you need to print in color.
public interface IWriteColor : IWrite, ISetColors
{
/// <summary>
/// writes out to the console using the requested color, resetting the color back to the console afterwards. Implementor Should be threadsafe.
/// </summary>
void Write(ConsoleColor color, string format, params object[] args);
/// <summary>
/// writes out to the console using the requested color, resetting the color back to the console afterwards. Implementor Should be threadsafe.
/// </summary>
void Write(ConsoleColor color, string text);
/// <summary>
/// writes out to the console using the requested color, resetting the color back to the console afterwards. Implementor Should be threadsafe.
/// </summary>
void WriteLine(ConsoleColor color, string format, params object[] args);
/// <summary>
/// writes out to the console using the requested color, resetting the color back to the console afterwards. Implementor Should be threadsafe.
/// </summary>
void WriteLine(ConsoleColor color, string text);
void Clear(ConsoleColor? backgroundColor);
}
Interface for a class that needs to print at a specific location in a window.
public interface IPrintAt
{
void PrintAt(int x, int y, string format, params object[] args);
void PrintAt(int x, int y, string text);
void PrintAt(int x, int y, char c);
}
public interface IPrintAtColor : IPrintAt, ISetColors
{
void PrintAtColor(ConsoleColor foreground, int x, int y, string text, ConsoleColor? background);
}
Interface for a class that needs to be able to scroll portions of the screen. This will most likely cause your library to require platform specific implementations for scrolling.
public interface IScrolling
{
void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft,
int targetTop, char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor);
void ScrollDown();
}
If you are writing a windowing library like Konsole
then each window region needs to report back an AbsoluteX and AbsoluteY position so that printing can happen at the correct (relative) position on the real console.
public interface IWindowed
{
/// <summary>
/// The absolute X position this window is located on the real or root console. This is where relative x:0 starts from for this window.
/// </summary>
int AbsoluteX { get; }
/// <summary>
/// The absolute Y position this window is located on the real or root console. This is where relative y:0 starts from for this window.
/// </summary>
int AbsoluteY { get; }
int WindowWidth { get; }
int WindowHeight { get; }
}
Interface for all the console methods that are most at risk of causing corruptions in multithreaded programs. The way to protect against corruption is to manage locking and manually save and restore state.
public interface IConsoleState : ISetColors
{
ConsoleState State { get; set; }
int CursorTop { get; set; }
int CursorLeft { get; set; }
/// <summary>
/// runs an action that may or may not modify the console state that can cause corruptions when thread context swaps. Must lock on a static locker, do try catch, and ensure state is back to what it was before the command ran.
/// </summary>
/// <param name="console"></param>
/// <param name="action"></param>
/// <remarks>If you're not writing a threadsafe control or threading is not an issue, then you can simply call action() in your implementation.</remarks>
/// <example>
/// <code>
/// lock(_locker)
/// {
/// var state = console.State;
/// try
/// {
/// action();
/// }
/// finally
/// {
/// console.State = state;</code>
/// }
/// }</example>
void DoCommand(IConsole console, Action action);
bool CursorVisible { get; set; }
}
void DoCommand(IConsole console, Action action)
Runs an action that may or may not modify the console state that can cause corruptions when thread context swaps. Must lock on a static locker, do try catch, and ensure state is back to what it was before the command ran. If you're not writing a threadsafe control or threading is not an issue, then you can simply call action()
in your implementation.
example implementation;
lock(_locker)
{
var state = console.State;
try
{
action();
}
finally
{
console.State = state;</code>
}
}
- Find code that writes to the
System.Console
directly. Do a grep search forConsole.*
to get started.
public class Accounts
{
... some code here
// methods below writes to the console
public void DoSomethingWithInvoice(Invoice inv) {
... some code
Console.WriteLine($"Invoice #{inv.Number}, Date: {inv.Date} Amount:{inv.Amount}");
...
}
}
- Install
IConsole
interfaces
install-package
Goblinfactory.Konsole
- Create your own live and test stubb, Mock or fake that implements
IConsole
. - Refactor your code to use the
IConsole
abstraction.IConsole
usesKonsole
as the parent namespace.
using Konsole;
public class Accounts
{
... some code here
private IConsole _console;
public Accounts(IConsole console) {
_console = console;
}
// change all writes to System.Console to use the injected IConsole
public void DoSomethingWithInvoice(Invoice inv) {
... some code
_console_.WriteLine($"Invoice #{inv.Number}, Date: {inv.Date} Amount:{inv.Amount}");
...
}
}
- now you can test your class
using Konsole;
[Test]
public void doing_something_to_invoices_must_print_the_invoice_details_to_the_console()
{
IConsole console = new MockConsole();
var accounts = new Accounts(console);
accounts.DoSomethingWithInvoice(TestData.MyTestInvoice1);
// confirm the display is what you expected
var expected = "Invoice #101, Date:21st February 2020, Amount:£ 1234.56";
console.Buffer.Should().BeEquivalentTo(expected);
}
Obviously because IConsole
is distributed as part of the main Goblinfactory.Konsole
library, you automatically have access to battle hardened threadsafe implementations of all of these interfaces, specifically;
ConcurrentWriter() : IConsole
NullWriter() : IConsole
Window() : IConsole
MockConsole() : IConsole
.. in addition to a full suite of Console library utilities, Box, Forms, ProgressBar, Windows and more that are all IConsole
compatible.
back to table of contents