This is a follow-up of my thread in Class SettingView
sending messages to a console in C#.
Here is the whole project on GitHub.
The starting point
After attending online courses covering configuration and dependency injection this is my attempt to transfer the knowledge in something I have designed. One of the goals is, to organize the files in separate projects and folders. Another goal is, to understand the mechanism of the configuration using appsettings.json
, appsettings.Production.json
, launchSettings.json
, secrets.json
and command line args
. However, the focus of this code review is described in the following sub-heading.
Code
Program.cs
using BasicCodingConsole.Views.MainView;
using BasicCodingConsole.Views.SettingView;
using BasicCodingLibrary.Models;
using BasicCodingLibrary.ViewModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", true)
.AddEnvironmentVariables()
.AddCommandLine(args);
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Build())
.Enrich.FromLogContext()
.WriteTo.File("LogFiles/apploggings.txt")
.CreateLogger();
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddLogging();
services.Configure<UserInformation>(builder.Build().GetSection("UserInformation"));
services.Configure<ApplicationInformation>(builder.Build().GetSection("ApplicationInformation"));
services.AddTransient<IMainView, MainView>();
services.AddTransient<IMainViewModel, MainViewModel>();
services.AddTransient<IAppSettingProvider, AppSettingProvider>();
services.AddTransient<ISettingView, SettingView>();
services.AddTransient<ISettingViewModel, SettingViewModel>();
})
.UseSerilog()
.Build();
var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
try
{
Log.Logger.Information("***** Run Application *****");
Log.Logger.Information($"EnvironmentVariable: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}");
Log.Logger.Information($"CommandLineArgument: {builder.Build().GetValue<string>("CommandLineArgument")}");
services.GetService<IMainView>()!
.Run();
}
catch (Exception e)
{
Log.Logger.Error("Unexpected Exception!", e);
Console.WriteLine("Unexpected Exception!");
Console.WriteLine(e);
Console.WriteLine($"\n***** Press ENTER To Continue *****");
Console.ReadLine();
}
finally
{
Log.Logger.Information("***** End Application *****");
}
The two views named MainView
and SettingView
The main goal in this code review request, however, is the implementation of interfaces and classes. My focus is on IMessage Message
. Here I tried to apply knowledge from courses on the SOLID principles. This pattern is new to me. I might be overkilling. And the chosen features may be very simple or sometimes just different in text output (check for view and app). However, it should be sufficient to understand these principles.
Diagram
Code
IMainView.cs
using BasicCodingConsole.ConsoleDisplays;
using BasicCodingConsole.ConsoleMenus;
using BasicCodingConsole.ConsoleMessages;
namespace BasicCodingConsole.Views.MainView;
public interface IMainView
{
IMenu Menu { get; }
IMessage Message { get; }
IDisplay Display { get; }
void Run();
}
MainView.cs
using BasicCodingConsole.ConsoleDisplays;
using BasicCodingConsole.ConsoleMenus;
using BasicCodingConsole.ConsoleMessages;
using BasicCodingConsole.Views.SettingView;
using BasicCodingLibrary.ViewModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using System.Diagnostics;
namespace BasicCodingConsole.Views.MainView;
public class MainView : ViewBase, IMainView
{
private readonly ILogger<MainView> _logger;
private readonly IConfiguration _configuration;
private readonly IMainViewModel _mainViewModel;
private readonly IHost _hostProvider;
/// <summary>
/// This property is providing the menu's content written to the console.
/// <para></para>
/// The content is implemented in file <see cref="ContentMainMenu"/>.
/// </summary>
public IMenu Menu => new MainMenu();
/// <summary>
/// This property is providing standard messages written to the console.
/// <para></para>
/// The content is implemented in the files <see cref="StartingApp"/>,
/// <see cref="EndingApp"/> and <see cref="ContinueMessage"/>
/// </summary>
public IMessage Message => new MainMessage();
/// <summary>
/// This property is providing standard method used to manipulate the console.
/// <para></para>
/// The method's behavior is implemented in the files <see cref="ClearingApp"/> and
/// <see cref="ResizingApp"/>.
/// </summary>
public IDisplay Display => new MainDisplay();
public MainView(ILogger<MainView> logger, IConfiguration configuration, IMainViewModel mainViewModel, IHost hostProvider)
{
Debug.WriteLine($"Passing <Constructor> in <{nameof(MainView)}>.");
_logger = logger;
_configuration = configuration;
_mainViewModel = mainViewModel;
_hostProvider = hostProvider;
}
public void Run()
{
Debug.WriteLine($"Passing <{nameof(Run)}> in <{nameof(MainView)}>.");
_logger.LogInformation("* Load: {view}", nameof(MainView));
Message.Start();
try
{
bool exitApp = false;
do
{
CheckWindowSize();
WriteMenu(Menu.CaptionItems, Menu.MenuItems, Menu.StatusItems);
switch (Console.ReadKey(true).Key)
{
case ConsoleKey.Escape:
exitApp = true;
break;
case ConsoleKey.A:
CheckWindowSize();
Action_A();
break;
case ConsoleKey.B:
CheckWindowSize();
Action_B();
break;
case ConsoleKey.C:
CheckWindowSize();
Action_C();
break;
default:
Console.Beep();
break;
}
} while (exitApp == false);
}
catch (Exception e)
{
Log.Logger.Error("Unexpected Exception!", e);
}
Message.End();
}
/// <summary>
/// This method starts the logic behind this menu item.
/// </summary>
private void Action_A()
{
Display.Clear();
WriteContent(); // to do
Message.Continue();
}
/// <summary>
/// This method starts the logic behind this menu item.
/// </summary>
private void Action_B()
{
// basics are working; improving needed
string processName = @"C:\Users\jenni\source\repos\MathSolutions\FibonacciApp\bin\Debug\net7.0\FibonacciApp.exe";
string processArgs = "";
//string processArgs = "--CommandLineArgument CommandLineStringIsPassedOnByBasicCodingConsole";
// get current console state
ConsoleColor foregroundColor = Console.ForegroundColor;
ConsoleColor backgroundColor = Console.BackgroundColor;
ProcessStartInfo startinfo = new ProcessStartInfo(processName, processArgs);
// Sync
startinfo.UseShellExecute = false;
Process p = Process.Start(startinfo)!;
p.WaitForExit();
// Async
//startinfo.UseShellExecute = true;
//var task = Process.Start(startinfo)!.WaitForExitAsync();
//if (task.IsCompleted)
//{
// task.Dispose(); // closing the window is not stopping the task, so this is needed!
//}
// set saved console state
Console.ForegroundColor = foregroundColor;
Console.BackgroundColor = backgroundColor;
}
/// <summary>
/// This method starts the logic behind this menu item.
/// </summary>
private void Action_C()
{
using var scope = _hostProvider.Services.CreateScope();
var services = scope.ServiceProvider;
services.GetService<ISettingView>()!.Run();
}
private void WriteContent()
{
_mainViewModel.Get(); // reloadOnChange? would it make this call unneccessary?
Console.WriteLine($"TestProperty - Default : {_configuration.GetConnectionString("Default")}");
Console.WriteLine($"TestProperty - ClassName: {_mainViewModel.ClassName}");
Console.WriteLine($"\nInformation about user <{_mainViewModel.AppSetting.UserInformation.NickName}>");
Console.WriteLine($"\tName : " +
$"{_mainViewModel.AppSetting.UserInformation.Person.FirstName} " +
$"{_mainViewModel.AppSetting.UserInformation.Person.LastName}");
Console.WriteLine($"\tGender: " +
$"{_mainViewModel.AppSetting.UserInformation.Person.Gender}");
Console.WriteLine($"\tID : " +
$"{_mainViewModel.AppSetting.UserInformation.Person.Id,4:0000}");
Console.WriteLine($"\nInformation about app <{nameof(BasicCodingConsole)}>");
Console.WriteLine($"\tLanguage : {_mainViewModel.AppSetting.ApplicationInformation.Language}");
Console.WriteLine($"\tLastLogin: {_mainViewModel.AppSetting.ApplicationInformation.LastLogin}");
Console.WriteLine($"\nInformation about command line arguments");
Console.WriteLine($"\tArguments: {_mainViewModel.AppSetting.CommandLineArgument}");
}
}
ISettingView.cs
using BasicCodingConsole.ConsoleDisplays;
using BasicCodingConsole.ConsoleMenus;
using BasicCodingConsole.ConsoleMessages;
namespace BasicCodingConsole.Views.SettingView;
public interface ISettingView
{
IMenu Menu { get; }
IMessage Message { get; }
IDisplay Display { get; }
void Run();
}
SettingView.cs
using BasicCodingConsole.ConsoleDisplays;
using BasicCodingConsole.ConsoleMenus;
using BasicCodingConsole.ConsoleMessages;
using BasicCodingLibrary.ViewModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace BasicCodingConsole.Views.SettingView;
public class SettingView : ViewBase, ISettingView
{
private readonly ILogger<SettingView> _logger;
private readonly IConfiguration _configuration;
private readonly ISettingViewModel _settingViewModel;
/// <summary>
/// This property is providing the menu's content written to the console.
/// <para></para>
/// The content is implemented in file <see cref="ContentSettingMenu"/>.
/// </summary>
public IMenu Menu => new SettingMenu();
/// <summary>
/// This property is providing standard messages written to the console.
/// <para></para>
/// The content is implemented in the files <see cref="StartingView"/>,
/// <see cref="EndingView"/> and <see cref="ContinueMessage"/>
/// </summary>
public IMessage Message => new SettingMessage();
/// <summary>
/// This property is providing standard method used to manipulate the console.
/// <para></para>
/// The method's behavior is implemented in the files <see cref="ClearingView"/> and
/// <see cref="ResizingView"/>.
/// </summary>
public IDisplay Display => new SettingDisplay();
public SettingView(ILogger<SettingView> logger, IConfiguration configuration, ISettingViewModel settingViewModel)
{
Debug.WriteLine($"Passing <Constructor> in <{nameof(SettingView)}>.");
_logger = logger;
_configuration = configuration;
_settingViewModel = settingViewModel; // to do
}
public void Run()
{
Debug.WriteLine($"Passing <{nameof(Run)}> in <{nameof(SettingView)}>.");
_logger.LogInformation("* Load: {view}", nameof(SettingView));
Message.Start();
WriteMenu(Menu.CaptionItems, Menu.MenuItems, Menu.StatusItems);
WriteContent(); // to do
Message.End();
}
private void WriteContent()
{
string message = "This is individual text by karwenzman!";
Console.WriteLine(message);
Console.WriteLine($"\nConnectionString (key = Default): {_configuration.GetConnectionString("Default")}");
}
}
ViewBase.cs
here the code to write a menu is implemented and available for all views inheriting it.
namespace BasicCodingConsole.Views;
public class ViewBase
{
/// <summary>
/// This property stores the height's minimum value of the menu's frame.
/// </summary>
internal const int frameHeightMinimum = 20;
/// <summary>
/// This property stores the width's minimum value of the menu's frame.
/// </summary>
internal const int frameWidthMinimum = 80;
/// <summary>
/// This property stores the messages to be written into the caption section.
/// </summary>
public string[]? captionMessages;
/// <summary>
/// This property stores the messages to be written into the menu section.
/// </summary>
public string[]? menuMessages;
/// <summary>
/// This property stores the messages to be written into the status section.
/// </summary>
public string[]? statusMessages;
/// <summary>
/// This property stores the character used for writing the menu's frame.
/// </summary>
public char frameCharacter;
/// <summary>
/// This property stores the height of the menu's frame.
/// </summary>
public int frameHeight;
/// <summary>
/// This property stores the width of the menu's frame.
/// </summary>
public int frameWidth;
/// <summary>
/// This method is writing the complete menu.
/// The menu's look is depending on the provided arguments.
/// </summary>
/// <param name="caption">The menu's header.</param>
/// <param name="menu">The menu's body.</param>
/// <param name="status">The menu's footer.</param>
/// <param name="frame">Character used to draw the frame. This is an optional argument.</param>
protected void WriteMenu(string[]? caption, string[]? menu, string[]? status, char frame = '*')
{
captionMessages = caption;
menuMessages = menu;
statusMessages = status;
frameCharacter = frame;
CheckWindowSize();
Console.Clear();
//Console.CursorVisible = false;
GetFrameWidth();
if (caption != null | menu != null | status != null)
{
WriteHorizontalFrame();
}
WriteCaptionMessages();
WriteMenuMessages();
WriteStatusMessage();
// TODO: sometimes the width is one character to wide;
// is this depending on debug, production or on the kind of distribution?
//Console.WriteLine($"Height - Console:{Console.WindowHeight,3} | Frame:{frameHeight,3}");
//Console.WriteLine($"Width - Console:{Console.WindowWidth,3} | Frame:{frameWidth,3}");
}
/// <summary>
/// This method is checking, if the console needs to be resized.
/// <para></para>
/// Resize may be necessary, if the user has manually altered the console size below
/// a defined value that is stored in <see cref="frameHeightMinimum"/> and
/// <see cref="frameWidthMinimum"/>.
/// </summary>
public void CheckWindowSize()
{
//Console.Clear();
GetFrameWidth();
}
/// <summary>
/// This method is drawing the content of the caption section.
/// <para>
/// Depending on the property <see cref="captionMessages"/> the content is drawn on screen.
/// If the property is <see href="Null"/>, there is no output on the screen.
/// </para>
/// </summary>
private void WriteCaptionMessages()
{
if (captionMessages == null)
{
return;
}
for (int i = 0; i < captionMessages.Length; i++)
{
string message = Convert.ToString(frameCharacter).PadRight(2);
message += captionMessages[i].PadRight(frameWidth - 4);
message += Convert.ToString(frameCharacter).PadLeft(2);
Console.WriteLine(message);
}
WriteHorizontalFrame();
}
/// <summary>
/// This method is drawing the content of the menu section.
/// <para>
/// Depending on the property <see cref="menuMessages"/> the content is drawn on screen.
/// If the property is <see href="Null"/>, there is no output on the screen.
/// </para>
/// </summary>
private void WriteMenuMessages()
{
if (menuMessages == null)
{
return;
}
for (int i = 0; i < menuMessages.Length; i++)
{
string message = Convert.ToString(frameCharacter).PadRight(2);
message += menuMessages[i].PadRight(frameWidth - 4);
message += Convert.ToString(frameCharacter).PadLeft(2);
Console.WriteLine(message);
}
WriteHorizontalFrame();
}
/// <summary>
/// This method is drawing the content of the status section.
/// <para>
/// Depending on the property <see cref="statusMessages"/> the content is drawn on screen.
/// If the property is <see href="Null"/>, there is no output on the screen.
/// </para>
/// </summary>
private void WriteStatusMessage()
{
if (statusMessages == null)
{
return;
}
for (int i = 0; i < statusMessages.Length; i++)
{
string message = Convert.ToString(frameCharacter).PadRight(2);
message += statusMessages[i].PadRight(frameWidth - 4);
message += Convert.ToString(frameCharacter).PadLeft(2);
Console.WriteLine(message);
}
WriteHorizontalFrame();
}
/// <summary>
/// This method is drawing a single horizontal frame.
/// <para>
/// Depending on the properties <see cref="frameCharacter"/>
/// and <see cref="frameWidth"/> the appearence will be adapted.
/// </para>
/// </summary>
private void WriteHorizontalFrame()
{
Console.WriteLine(frameCharacter.ToString().PadLeft(frameWidth, frameCharacter));
}
/// <summary>
/// This method is checking <see cref="Console.WindowHeight"/> and <see cref="Console.WindowWidth"/>
/// and altering the menu's appearance accordingly.
/// </summary>
private void GetFrameWidth()
{
bool isResize = false;
frameHeight = Console.WindowHeight;
frameWidth = Console.WindowWidth; // when published the behaviour is different to debug mode
if (frameHeight < frameHeightMinimum)
{
frameHeight = frameHeightMinimum;
isResize = true;
}
if (frameWidth < frameWidthMinimum)
{
frameWidth = frameWidthMinimum;
isResize = true;
}
try
{
if (isResize)
{
// how can I make this available on all OS? not only windows?
Console.Clear();
Console.SetWindowSize(frameWidth, frameHeight);
}
}
catch (Exception e)
{
Console.Clear();
Console.WriteLine(e.ToString());
Console.WriteLine("An exception was raised! Press ENTER to continue.");
Console.ReadLine();
}
}
}
Interface IMessage.cs
and all depending classes / interfaces
Since the design of IDisplay
is identical to IMessage
I am skipping to put the code here. In this code review I just want to focus on IMessage
.
Diagram
Code
The idea here is to have one class providing members for one view. These memeber's behavior can be altered by instancing specific classes. e.g. IEnding ending = new EndingApp();
and IEnding ending = new EndingView();
. And if I have to update the content of a message, I just need to update e.g. the class EndingApp
. This should be a implementation of the open-closed principle and the single-responsibility principle.
IMessage
namespace BasicCodingConsole.ConsoleMessages;
public interface IMessage : IContinuing, IEnding, IStarting
{
}
MainMessage.cs
namespace BasicCodingConsole.ConsoleMessages;
public class MainMessage : IMessage
{
public void Continue()
{
IContinuing continuing = new ContinueMessage();
continuing.Continue();
}
public void End()
{
IEnding ending = new EndingApp();
ending.End();
}
public void Start()
{
IStarting starting = new StartingApp();
starting.Start();
}
}
SettingMessage.cs
namespace BasicCodingConsole.ConsoleMessages;
public class SettingMessage : IMessage
{
public void Continue()
{
IContinuing continuing = new ContinueMessage();
continuing.Continue();
}
public void End()
{
IEnding ending = new EndingView();
ending.End();
}
public void Start()
{
IStarting starting = new StartingView();
starting.Start();
}
}
IContinuing.cs
namespace BasicCodingConsole.ConsoleMessages;
public interface IContinuing
{
void Continue();
}
ContinueMessage.cs
namespace BasicCodingConsole.ConsoleMessages;
public class ContinueMessage : IContinuing
{
public void Continue()
{
Console.WriteLine($"\nPress ENTER to continue...");
Console.ReadLine();
}
}
IEnding.cs
namespace BasicCodingConsole.ConsoleMessages;
public interface IEnding
{
/// <summary>
/// Writing a standard message to the console and awaiting user input.
/// Then, clearing the console.
/// </summary>
void End();
}
EndingApp.cs
namespace BasicCodingConsole.ConsoleMessages;
public class EndingApp : IEnding
{
public void End()
{
Console.WriteLine($"\nYou are going to leave the app. Press ENTER to end the app...");
Console.ReadLine();
Console.Clear();
}
}
EndingView.cs
namespace BasicCodingConsole.ConsoleMessages;
public class EndingView : IEnding
{
public void End()
{
Console.WriteLine($"\nYou are going to leave the view. Press ENTER to continue...");
Console.ReadLine();
Console.Clear();
}
}
IStarting.cs
namespace BasicCodingConsole.ConsoleMessages;
public interface IStarting
{
/// <summary>
/// Writing a standard message to the console and awaiting user input.
/// Then, clearing the console.
/// </summary>
void Start();
}
StartingApp.cs
namespace BasicCodingConsole.ConsoleMessages;
public class StartingApp : IStarting
{
public void Start()
{
string message = $"You are going to start the app.";
Console.WriteLine(message);
Console.WriteLine("=".PadLeft(message.Length, '='));
Console.WriteLine($"\nPress ENTER to continue...");
Console.ReadLine();
Console.Clear();
}
}
StartingView.cs
namespace BasicCodingConsole.ConsoleMessages;
public class StartingView : IStarting
{
public void Start()
{
string message = $"You are going to start the view.";
Console.WriteLine(message);
Console.WriteLine("=".PadLeft(message.Length, '='));
Console.WriteLine($"\nPress ENTER to continue...");
Console.ReadLine();
Console.Clear();
}
}
I suppose this is a lot of content and I will stop here. Mentoring is much appreciated.