4
\$\begingroup\$

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

image

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

image

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.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Just FYI, this website is a question and answer website, it is not a list of threads. This is why you have to ask a follow up question rather than alter the code in the question and ask for another review. \$\endgroup\$
    – pacmaninbw
    Jul 11, 2023 at 17:45
  • 1
    \$\begingroup\$ Wait 2 days before accepting an answer. The size of the question is going to slow down the reviews. \$\endgroup\$
    – pacmaninbw
    Jul 12, 2023 at 12:31

2 Answers 2

4
\$\begingroup\$

ViewBase

Here we see copy&pasta isn't always a good coding style although we all do it sometimes to save time, but one should just check again if what we copied and pasted is what we want. What I am talking about are the comments to the const's which are just lying

/// <summary>
/// This property stores the height's minimum value of the menu's frame.
/// </summary>
internal const int frameHeightMinimum = 20

It just isn't a property is it?

Let us look a few lines further...

/// <summary>
/// This property stores the messages to be written into the caption section.
/// </summary>
public string[]? captionMessages;

Well, this isn't a property either. This is a public field which can be accessed and filled without validation. In addition based on the Naming-Guidelines

DO use PascalCasing for all public member, type, and namespace names consisting of multiple words.

this field should be named CaptionMessages.

Well this where just some basics but I think you have a bigger problem. Why do you have this fields at all? What value are they having for your application? I just don't see any value at least not in your current implementation.

Let's take a look at the main-purpose of this class from the outside. If one creates an instance of this class there is only one method visible: CheckWindowSize()

The first question is, why is CheckWindowSize() public at all? You are calling this method from classes which inherit ViewBase hence it would be sufficient to use the protected access modifier.

The next question is, why is this method just calling GetFrameWidth()? If you or Sam the maintainer would need to go bug-hunting you would need to jump around in the code-base to check what this method is doing.

Now, about GetFrameWidth(), this method is solely by it's name lying. It's documentation states

This method is checking and and altering the menu's appearance accordingly.

It doesn't get the frame width, it is checking if the height of the current console is smaller than frameHeightMinimum and it is checking if the width of the current console is smaller than frameWidthMinimum. The first question coming to my mind is, what about maximum dimensions.

I would extract the checking if resizing is needed into a separate method like

private bool IsViewResizingNeeded()
{
    bool isResizingNeeded = false;
    frameHeight = Console.WindowHeight;
    frameWidth = Console.WindowWidth; 

    if (frameHeight < frameHeightMinimum)
    {
        frameHeight = frameHeightMinimum;
        isResizingNeeded = true;
    }

    if (frameWidth < frameWidthMinimum)
    {
        frameWidth = frameWidthMinimum;
        isResizingNeeded = true;
    }
    return isResizingNeeded;
}

And would name the calling ResizeViewIfNeeded() and would implement it like

protected void ResizeViewIfNeeded()
{
    if (IsViewResizingNeeded())
    {
        Console.Clear();
        Console.SetWindowSize(frameWidth, frameHeight);
    }
}

As you see I have omitted the try..catch because you would better validate the values frameWidth and frameHeight before calling Console.SetWindowSize(). A prevented exception is better than a caught exception.

Now the CheckWindowSize() would be superfluous hence we should remove it.

This would leave the class with no public method at all.

Because you don't use this class as standalone but inherit it, why don't you make it abstract?

WriteMenu()

The name of the method is clear and we know from its name what it should do, but why are that many arguments passed to it when there is an IMenu interface which I would expect to have properties for CaptionItems, MenuItems and StatusItems.

The methods definition could then just look like

protected void WriteMenu(IMenu menu, char frame = '*')

This

if (caption != null | menu != null | status != null)  

will check every condition although it would be enough if one condition would be true.

Passing the frameCharacter to WriteHorizontalFrame(), WriteCaptionMessages, WriteMenuMessages() and WriteStatusMessage() would make the field frameCharacter superfluous .

Passing the introduced menu argument to WriteCaptionMessages, WriteMenuMessages() and WriteStatusMessage() would make the fields captionMessages, menuMessages and statusMessages superfluous as well.

Why does the method WriteStatusMessage() not use the plural form???

Concatenation of strings in a loop is not a good idea because it will create new strings all the time. It is better to use a StringBuilder object instead.

But while we are refactoring these methods we see that each method does indeed the same thing, they are just named differently. I would suggest to have only one method named WriteMessages().

Implementing these changes will lead to

private void WriteHorizontalFrame(char frameCharacter = '*')
{
    Console.WriteLine("".PadLeft(frameWidth, frameCharacter));
}

private const char framePadding = ' ';
private void WriteMessages(string[] messages, char frameCharacter = '*')
{
    if (messages == null) { return; }

    StringBuilder sb = new StringBuilder();
    foreach (var message in messages)
    {
        sb.Append(frameCharacter)
          .Append(framePadding)
          .Append(message.PadRight(frameWidth - 4))
          .Append(framePadding)
          .Append(frameCharacter)
          .AppendLine();
    }

    Console.Write(sb.ToString());

    WriteHorizontalFrame();
}  

and the WriteMenu() method would look like so

    protected void WriteMenu(IMenu menu, char frameCharacter = '*')
    {
        Console.Clear();
        ResizeViewIfNeeded();

        if (menu.CaptionItems != null || menu.MenuItems != null || menu.StatusItems != null)
        {
            WriteHorizontalFrame(frameCharacter);
        }
        WriteMessages(menu.CaptionItems, frameCharacter);
        WriteMessages(menu.MenuItems, frameCharacter);
        WriteMessages(menu.StatusItems, frameCharacter);

    } 
\$\endgroup\$
2
  • \$\begingroup\$ Wow, thanks! That is very helpful. There is a lot I have to think about. \$\endgroup\$
    – karwenzman
    Jul 12, 2023 at 12:27
  • \$\begingroup\$ Rework done. It is all in this issue on my GitHub project. I am not sure if I should update my code in this post, so everything went into GitHub. Pointing your finger on it, it was so obvious that the three WriteSomethingMessages are doing the same stuff - not being DRY. Again, thanks a lot! \$\endgroup\$
    – karwenzman
    Jul 12, 2023 at 15:09
2
\$\begingroup\$

Enhenced state

Picking up the hint of @Heslacher I have re-designed the application. One idea is, to check both - the minimum size of the console and the maximum size of it. Another idea is, to make it possible to change the min/max values via configuration files.

It is possible, now, to set the values in two places. Hardcoded within the application (and every view can have its own values). This works also as fall back value, if the second option is not used or not activated. Because, if appsettings.json does not provide min/max values or these values are <= 0 the hardcoded values are used.

Additionally, I have tested several ways to use the dependency injection system. Where to create new instances of used properties, where to update properties or where to use the Get() or Run() methods. So far, I am preferring to put all the creation of instances into the constructor to have one place to look at. Since I am using AddTransient to add the SettingView to the DI, the constructor seems to be a proper place to read the configuration files into the AppSettingModel instance.

One question refering to configuration is open to me: what effect does the parameter reloadOnChange have? See this code snippet .AddJsonFile("appsettings.json",optional: false, reloadOnChange: true). My answer to this is, that it is working in environments like WPF using the PropertyChanged and binding properties to the view.

Please correct me, if I am writing something stupid here or if my thoughts are leading me into a wrong direction. Mentoring is much appreciated.

Summary

  • re-design of ViewBase
  • implementing IMenuBehavior to provide members of console min/max height/width
  • implementing members in appsettings.json to store values of console min/max height/width
  • implementing members in AppSettingModel to store the configuration values
  • re-design of SettingView and MainView to run with these new features (I just put the classes needed for the SettingView in this comment)

Code

ISettingView.cs (no changes)

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 (updated)

using BasicCodingConsole.ConsoleDisplays;
using BasicCodingConsole.ConsoleMenus;
using BasicCodingConsole.ConsoleMessages;
using BasicCodingLibrary.Models;
using BasicCodingLibrary.Providers;
using Microsoft.Extensions.Logging;
using System.Diagnostics;

namespace BasicCodingConsole.Views.SettingView;

public class SettingView : ViewBase, ISettingView
{
    private readonly IAppSettingProvider _appSettingProvider;
    private readonly ILogger<SettingView> _logger;

    /// <summary>
    /// This property is providing the information collected from configuration.
    /// </summary>
    public IAppSettingModel AppSettingModel { get; set; }
    /// <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 { get; set; }
    /// <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 { get; set; }
    /// <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 { get; set; }

    public SettingView(IAppSettingProvider appSettingProvider, ILogger<SettingView> logger)
    {
        _appSettingProvider = appSettingProvider;
        _logger = logger;

        AppSettingModel = new AppSettingModel();
        AppSettingModel = _appSettingProvider.Get();

        Display = new SettingDisplay();
        Menu = new SettingMenu(AppSettingModel);
        Message = new SettingMessage();

        _logger.LogInformation("* Load: {view}", nameof(SettingView));
    }

    public void Run()
    {
        Message.Start(showMessage: false, clearScreen: true);
        WriteMenu(Menu);
        WriteContent();
        Message.End(showMessage: true, clearScreen: true);
    }

    /// <summary>
    /// This method is writing the members of <see cref="AppSettingModel"/> to a console.
    /// </summary>
    private void WriteContent()
    {
        Console.WriteLine($"\nInformation about user <{AppSettingModel.UserInformation.NickName}>");
        Console.WriteLine($"\tName  : " +
            $"{AppSettingModel.UserInformation.Person.FirstName} " +
            $"{AppSettingModel.UserInformation.Person.LastName}");
        Console.WriteLine($"\tGender: " +
            $"{AppSettingModel.UserInformation.Person.Gender}");
        Console.WriteLine($"\tID    : " +
            $"{AppSettingModel.UserInformation.Person.Id,4:0000}");

        Console.WriteLine($"\nInformation about app <{nameof(BasicCodingConsole)}>");
        Console.WriteLine($"\tLanguage : {AppSettingModel.ApplicationInformation.Language}");
        Console.WriteLine($"\tLastLogin: {AppSettingModel.ApplicationInformation.LastLogin}");
        Console.WriteLine($"\tMaxHeight: {AppSettingModel.ApplicationInformation.ConsoleHeightMaximum}");
        Console.WriteLine($"\tMinHeight: {AppSettingModel.ApplicationInformation.ConsoleHeightMinimum}");
        Console.WriteLine($"\tMaxWidth : {AppSettingModel.ApplicationInformation.ConsoleWidthMaximum}");
        Console.WriteLine($"\tMinWidth : {AppSettingModel.ApplicationInformation.ConsoleWidthMinimum}");
        Console.WriteLine($"\nInformation about command line arguments");
        Console.WriteLine($"\tArguments: {AppSettingModel.CommandLineArgument}");
        Console.WriteLine($"\nInformation about connection strings");
        Console.WriteLine($"\tDefault: {AppSettingModel.ConnectionString}");

    }
}

ViewBase.cs (updated)

using BasicCodingConsole.ConsoleMenus;
using System.Runtime.InteropServices;
using System.Text;

namespace BasicCodingConsole.Views;

public abstract class ViewBase
{
    /// <summary>
    /// This const stores the padding character needed to write the menu items.
    /// </summary>
    internal const char framePadding = ' ';

    /// <summary>
    /// This method is writing the complete menu.
    /// The menu's look is depending on the provided arguments.
    /// <para></para>
    /// Before writing the menu 
    /// <br></br>- a check of the console's size is performed
    /// <br></br>- the console is cleared
    /// </summary>
    /// <param name="menu">The menu's content.</param>
    /// <param name="frameCharacter">Character used to draw the frame. This is an optional argument.</param>
    protected void WriteMenu(IMenu menu, char frameCharacter = '*')
    {
        ResizeConsoleIfNeededAndClearIt(menu);

        if (menu.CaptionItems != null || menu.MenuItems != null || menu.StatusItems != null)
        {
            WriteHorizontalFrame();
        }

        WriteItems(menu.CaptionItems, frameCharacter);
        WriteItems(menu.MenuItems, frameCharacter);
        WriteItems(menu.StatusItems, frameCharacter);

        // TODO: sometimes the width is one character to wide;
        // is this depending on debug, production or on the kind of distribution?
        Console.WriteLine($"ConsoleHeight:{Console.WindowHeight,3}");
        Console.WriteLine($"ConsoleWidth :{Console.WindowWidth,3}");
    }

    /// <summary>
    /// This method is checking, if the console needs to be resized.
    /// If true, the console is resized to the minimum size it should have.
    /// Finally the console is cleared.
    /// <br></br>
    /// Currently resizing is supported for WindowsOS, only.
    /// <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>
    protected void ResizeConsoleIfNeededAndClearIt(IMenu menu)
    {
        if (IsConsoleSizeSmallerThenMinimum(menu))
        {
            ResizeConsole(menu.ConsoleWidthMinimum, menu.ConsoleHeightMinimum);
        }
        else if (IsConsoleSizeBiggerThenMaximum(menu))
        {
            ResizeConsole(menu.ConsoleWidthMaximum, menu.ConsoleHeightMaximum);
        }
        Console.Clear();
    }

    /// <summary>
    /// This method is actually performing the resizing of the console.
    /// </summary>
    /// <param name="consoleWidth"></param>
    /// <param name="consoleHeight"></param>
    private void ResizeConsole(int consoleWidth, int consoleHeight)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            Console.SetWindowSize(consoleWidth, consoleHeight);
        }
        else
        {
            // TODO: how can I make this available on all OS? not only windows?
            Console.WriteLine("Sorry, resizing is not implemented for your OS.");
        }
    }

    /// <summary>
    /// This method is drawing a single horizontal frame.
    /// </summary>
    /// <param name="frameCharacter">Setting the frame's appearance.</param>
    private void WriteHorizontalFrame(char frameCharacter = '*')
    {
        Console.WriteLine("".PadLeft(Console.WindowWidth, frameCharacter));
    }

    /// <summary>
    /// This method is checking if the current console is smaller then a defined value 
    /// that is stored in <see cref="IMenuBehavior.ConsoleHeightMinimum"/> and <see cref="IMenuBehavior.ConsoleWidthMinimum"/>.
    /// </summary>
    /// <returns>True, if resize is necessary.</returns>
    private bool IsConsoleSizeSmallerThenMinimum(IMenu menu)
    {
        bool isResize = false;
        if (Console.WindowHeight < menu.ConsoleHeightMinimum)
        {
            isResize = true;
        }
        else if (Console.WindowWidth < menu.ConsoleWidthMinimum)
        {
            isResize = true;
        }
        return isResize;
    }

    /// <summary>
    /// This method is checking if the current console is size bigger then a defined value 
    /// that is stored in <see cref="IMenuBehavior.ConsoleHeightMaximum"/> and <see cref="IMenuBehavior.ConsoleWidthMaximum"/>.
    /// </summary>
    /// <returns>True, if resize is necessary.</returns>
    private bool IsConsoleSizeBiggerThenMaximum(IMenu menu)
    {
        bool isResize = false;
        if (Console.WindowHeight > menu.ConsoleHeightMaximum)
        {
            isResize = true;
        }
        else if (Console.WindowWidth > menu.ConsoleWidthMaximum)
        {
            isResize = true;
        }
        return isResize;
    }

    /// <summary>
    /// This method is writing the items appending the menu.
    /// <para></para>
    /// If there are items to be written to the console,
    /// then a horizontal frame will be appended.
    /// </summary>
    /// <param name="items"></param>
    /// <param name="frameCharacter"></param>
    private void WriteItems(string[]? items, char frameCharacter = '*')
    {
        if (items == null)
        {
            return;
        }

        StringBuilder sb = new StringBuilder();
        foreach (var message in items)
        {
            sb.Append(frameCharacter)
              .Append(framePadding)
              .Append(message.PadRight(Console.WindowWidth - 4))
              .Append(framePadding)
              .Append(frameCharacter)
              .AppendLine();
        }
        Console.Write(sb);

        WriteHorizontalFrame();
    }
}

IMenuBehavior (updated)

namespace BasicCodingConsole.ConsoleMenus;

/// <summary>
/// This interface is providing members to setup the menu's behavior.
/// </summary>
public interface IMenuBehavior
{
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleHeightMaximum { get; set; }
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleHeightMinimum { get; set; }
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleWidthMaximum { get; set; }
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleWidthMinimum { get; set; }
}

BehaviorSettingMenu.cs (new)
Here the values are hardcoded. They work as fallback values, if no valid values are provided in the configuration files.

namespace BasicCodingConsole.ConsoleMenus;

public class BehaviorSettingMenu : IMenuBehavior
{
    public int ConsoleHeightMaximum { get; set; } = 40;
    public int ConsoleHeightMinimum { get; set; } = 40;
    public int ConsoleWidthMaximum { get; set; } = 120;
    public int ConsoleWidthMinimum { get; set; } = 120;
}

appsettings.json (updated)
If values are > 0, then they will be used to control the console size. If not, then the fallback values from the file BehaviorSettingMenu.cs will be used.

{
  "UserInformation": {
    "NickName": "karwenzman (stored in <appsettings.json> file)",
    "Person": {
      "Id": 9999,
      "FirstName": "This is a",
      "LastName": "fake name (stored in <appsettings.json> file)",
      "Gender": 0
    }
  },
  "ApplicationInformation": {
    "LastLogin": "today (stored in <appsettings.json> file)",
    "Language": "en-US (stored in <appsettings.json> file)",
    "ConsoleHeightMaximum": 40,
    "ConsoleHeightMinimum": 20,
    "ConsoleWidthMaximum": 160,
    "ConsoleWidthMinimum": 80
  },
  "ConnectionStrings": {
    "Default": "ConnectionString (stored in <appsettings.json> file)"
  },
  "CommandLineArgument": "CommandLine string (stored in <appsettings.json> file)"
}

IMenu.cs (no changes)

namespace BasicCodingConsole.ConsoleMenus;

/// <summary>
/// This interface is putting things together.
/// <br></br>- implementing <see cref="IMenuBehavior"/>
/// <br></br>- implementing <see cref="IMenuContent"/>
/// </summary>
public interface IMenu : IMenuBehavior, IMenuContent
{

}

SettingMenu.cs (updated)
Two constructors provided different menu behavior. During implementation it can be chosen between using the hardcoded values only. Or to check for valid values in the configuration files.

using BasicCodingLibrary.Models;

namespace BasicCodingConsole.ConsoleMenus;

public class SettingMenu : IMenu
{
    public string[]? CaptionItems { get; set; }
    public string[]? MenuItems { get; set; }
    public string[]? StatusItems { get; set; }
    public int ConsoleHeightMaximum { get; set; }
    public int ConsoleHeightMinimum { get; set; }
    public int ConsoleWidthMaximum { get; set; }
    public int ConsoleWidthMinimum { get; set; }

    /// <summary>
    /// This parameterless constructor reads the console size values
    /// from a class implementing <see cref="IMenuBehavior"/>.
    /// </summary>
    public SettingMenu()
    {
        IMenuContent content = new ContentSettingMenu();
        CaptionItems = content.CaptionItems;
        MenuItems = content.MenuItems;
        StatusItems = content.StatusItems;

        IMenuBehavior behavior = new BehaviorSettingMenu();
        ConsoleHeightMaximum = behavior.ConsoleHeightMaximum;
        ConsoleHeightMinimum = behavior.ConsoleHeightMinimum;
        ConsoleWidthMaximum = behavior.ConsoleWidthMaximum;
        ConsoleWidthMinimum = behavior.ConsoleWidthMinimum;
    }

    /// <summary>
    /// This constructor with parameter reads the console size values
    /// from a class implementing <see cref="IAppSettingModel"/>.
    /// <para></para>
    /// The fallback values are read from a class implementing <see cref="IMenuBehavior"/>,
    /// <br></br>if:
    /// <br></br>- the instance of <see cref="AppSettingModel"/> is null
    /// <br></br>- a value of the height or width is &lt;= 0
    /// </summary>
    /// <param name="appSettingModel"></param>
    public SettingMenu(IAppSettingModel appSettingModel)
    {
        IMenuContent content = new ContentSettingMenu();
        CaptionItems = content.CaptionItems;
        MenuItems = content.MenuItems;
        StatusItems = content.StatusItems;

        IMenuBehavior behavior = new BehaviorSettingMenu();
        ConsoleHeightMaximum = behavior.ConsoleHeightMaximum;
        ConsoleHeightMinimum = behavior.ConsoleHeightMinimum;
        ConsoleWidthMaximum = behavior.ConsoleWidthMaximum;
        ConsoleWidthMinimum = behavior.ConsoleWidthMinimum;

        if (appSettingModel != null)
        {
            if (appSettingModel.ApplicationInformation.ConsoleHeightMaximum > 0)
            {
                ConsoleHeightMaximum = appSettingModel.ApplicationInformation.ConsoleHeightMaximum;
            }
            if (appSettingModel.ApplicationInformation.ConsoleHeightMinimum > 0)
            {
                ConsoleHeightMinimum = appSettingModel.ApplicationInformation.ConsoleHeightMinimum;
            }
            if (appSettingModel.ApplicationInformation.ConsoleWidthMaximum > 0)
            {
                ConsoleWidthMaximum = appSettingModel.ApplicationInformation.ConsoleWidthMaximum;
            }
            if (appSettingModel.ApplicationInformation.ConsoleWidthMinimum > 0)
            {
                ConsoleWidthMinimum = appSettingModel.ApplicationInformation.ConsoleWidthMinimum;
            }
        }
    }
}

IAppSettingProvider.cs (no changes)

using BasicCodingLibrary.Models;

namespace BasicCodingLibrary.Providers;

/// <summary>
/// This interface is providing the access to the class <see cref="AppSettingModel"/>.
/// </summary>
public interface IAppSettingProvider
{
    /// <summary>
    /// This method is getting the current values of <see cref="AppSettingModel"/>.
    /// <para>
    /// A new snapshot of the file 'appsettings.json' should be implemented from derived classes.
    /// </para>
    /// </summary>
    /// <returns>An instance of class <see cref="AppSettingModel"/>.</returns>
    AppSettingModel Get();
}

AppSettingProvider.cs (updated)
reading from the current configuration files, whether it is appsettings.json, appsettings.Production.json, etc.

using BasicCodingLibrary.Models;
using Microsoft.Extensions.Configuration;

namespace BasicCodingLibrary.Providers;

/// <summary>
/// This class is providing services for instances of <see cref="AppSettingModel"/>.
/// </summary>
public class AppSettingProvider : IAppSettingProvider
{
    private readonly IConfiguration _configuration;

    public AppSettingProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    /// <summary>
    /// This method is getting the current values from the configuration files.
    /// </summary>
    /// <returns>An instance of class <see cref="AppSettingModel"/> is returned.</returns>
    public AppSettingModel Get()
    {
        AppSettingModel output = new()
        {
            CommandLineArgument = _configuration.GetValue<string>("CommandLineArgument")!,
            ConnectionString = _configuration.GetConnectionString("Default")!,
            ApplicationInformation = _configuration.GetSection("ApplicationInformation").Get<ApplicationInformation>()!,
            UserInformation = _configuration.GetSection("UserInformation").Get<UserInformation>()!
        };

        return output;
    }
}

The Model

IAppSettingModel.cs

namespace BasicCodingLibrary.Models;

/// <summary>
/// This interface is providing members to setup the class <see cref="AppSettingModel"/>.
/// </summary>
public interface IAppSettingModel
{
    /// <summary>
    /// This property is providing user information received from configuration.
    /// </summary>
    public UserInformation UserInformation { get; set; }
    /// <summary>
    /// This property is providing application information received from configuration.
    /// </summary>
    public ApplicationInformation ApplicationInformation { get; set; }
    /// <summary>
    /// This property is providing the command line arguments received from configuration.
    /// </summary>
    public string CommandLineArgument { get; set; }
    /// <summary>
    /// This property is providing the connection string received from configuration.
    /// </summary>
    public string ConnectionString { get; set; }
}

AppSettingModel

namespace BasicCodingLibrary.Models;

/// <summary>
/// This class is providing members to access the <b>appsettings.json</b> file.
/// </summary>
public class AppSettingModel : IAppSettingModel
{
    /// <summary>
    /// This property is providing user information received from configuration.
    /// </summary>
    public UserInformation UserInformation { get; set; } = new UserInformation();
    /// <summary>
    /// This property is providing application information received from configuration.
    /// </summary>
    public ApplicationInformation ApplicationInformation { get; set; } = new ApplicationInformation();
    /// <summary>
    /// This property is providing the command line arguments received from configuration.
    /// </summary>
    public string CommandLineArgument { get; set; } = "default";
    /// <summary>
    /// This property is providing the connection string received from configuration.
    /// </summary>
    public string ConnectionString { get; set; } = "default";
}

UserInformation.cs

namespace BasicCodingLibrary.Models;

/// <summary>
/// This class is providing a section of the <b>appsettings.json</b> file.
/// </summary>
public class UserInformation
{
    /// <summary>
    /// The user's nickname.
    /// </summary>
    public string NickName { get; set; } = "default";
    /// <summary>
    /// The user's personal information.
    /// </summary>
    public Person Person { get; set; } = new Person();
}

ApplicationInformation.cs (updated)
Here the properties to store the min/max values are added.

namespace BasicCodingLibrary.Models;

/// <summary>
/// This class is providing a section of the <b>appsettings.json</b> file.
/// </summary>
public class ApplicationInformation
{
    /// <summary>
    /// Not used, yet. Testing only.
    /// </summary>
    public string LastLogin { get; set; } = "default";
    /// <summary>
    /// Not used, yet. Testing only.
    /// </summary>
    public string Language { get; set; } = "default";
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleHeightMaximum { get; set; }
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleHeightMinimum { get; set; }
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleWidthMaximum { get; set; }
    /// <summary>
    /// A value used to control the console size.
    /// </summary>
    public int ConsoleWidthMinimum { get; set; }
}

Person.cs

using BasicCodingLibrary.Enums;

namespace BasicCodingLibrary.Models;

/// <summary>
/// This class is providing members to describe a <see cref="Person"/>.
/// </summary>
public class Person
{
    /// <summary>
    /// The person's unique ID.
    /// </summary>
    public int Id { get; set; } = 0;
    /// <summary>
    /// The person's first name.
    /// </summary>
    public string FirstName { get; set; } = "default";
    /// <summary>
    /// The person's last name.
    /// </summary>
    public string LastName { get; set; } = "default";
    /// <summary>
    /// The person's gender.
    /// </summary>
    public Gender Gender { get; set; } = Gender.unknown;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.