Az MVC flexibilitását jól demonstrálja, hogy akár egy konzol alkalmazásban is alkalmazhatjuk. Nézzünk erre egy példát:
using System;
using System.Collections.Generic;
namespace MVCDemo;
//Model
internal class TaskModel
{
public string Description { get; set; }
public bool IsCompleted { get; set; }
public TaskModel()
{
Description = string.Empty;
}
}
//Szegregációs interfészek a közvetlen
//függőségek elkerülése végett
internal interface ITaskView
{
//Interfészben specifikus kollekció helyett
//a kollekciót leíró interfészt célszerű használni
void ShowTaskList(IList<TaskModel> tasks);
T Read<T>(string prompt) where T : IConvertible;
void Clear();
void WriteLine(string message);
}
// Controller
class TaskController
{
private readonly List<TaskModel> _tasks;
private readonly ITaskView _view;
public TaskController(ITaskView view)
{
_view = view;
_tasks = new List<TaskModel>();
}
//Task létrehozása
public void AddTask(string taskDescription)
{
_tasks.Add(new TaskModel
{
Description = taskDescription,
IsCompleted = false
});
}
//Megjelölés készként
public void MarkTaskAsCompleted(int taskIndex)
{
if (taskIndex >= 0 && taskIndex < _tasks.Count)
{
_tasks[taskIndex].IsCompleted = true;
}
}
public void Run()
{
while (true)
{
_view.Clear();
_view.ShowTaskList(_tasks);
_view.WriteLine("\nOptions:");
_view.WriteLine("1. Add Task");
_view.WriteLine("2. Mark Task as Completed");
_view.WriteLine("3. Exit");
string choice = _view.Read<string>("Enter your choice: ");
switch (choice)
{
case "1":
string taskDescription = _view.Read<string>("Task description: ");
AddTask(taskDescription);
break;
case "2":
int taskIndex = _view.Read<int>("Task index to mark as completed: ");
MarkTaskAsCompleted(taskIndex - 1);
break;
case "3":
Environment.Exit(0);
break;
default:
Console.WriteLine("Invalid choice. Please try again.");
break;
}
}
}
}
//View
internal class TaskView : ITaskView
{
public void Clear()
=> Console.Clear();
// Adat olvasás - UI specifikus logika
public T Read<T>(string prompt) where T : IConvertible
{
Console.Write(prompt);
string? input = Console.ReadLine() ?? string.Empty;
return (T)Convert.ChangeType(input, typeof(T));
}
public void ShowTaskList(IList<TaskModel> tasks)
{
Console.WriteLine("Task List:");
for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
Console.WriteLine($"{i + 1}. [{(task.IsCompleted ? 'X' : ' ')}] {task.Description}");
}
}
public void WriteLine(string message)
=> Console.WriteLine(message);
}
internal static class Program
{
static void Main(string[] args)
{
//Komponensek létrehozása
ITaskView view = new TaskView();
TaskController controller = new TaskController(view);
//futtatás
controller.Run();
}
}
A fenti példa egy egyszerű feladatlista alkalmazást valósít meg a konzolon. A TaskModel osztály feladata, hogy egy elvégzendő feladatot reprezentáljon. Az elnevezéséből és a funkciójából is adódik, hogy ez a modell rétegünk.
A kontroller réteget a TaskController valósítja meg. Itt található az alkalmazás fő logikája, ami a feladatok kezeléséhez kapcsolódik. Az egyszerűség kedvéért csupán két funkciót biztosít: Létre tudunk hozni egy feladatot és egy meglévőt készre tudunk állítani. A kontroller függősége a view, mivel ezen keresztül történnek az interakciók a felhasználóval, de hogy ne legyen közvetlen függőség a view és a controller között, bevezettem szegregációs céllal a ITaskView interfészt. Ez az interfész leír minden olyan funkciót, amire a controller támaszkodik a View oldalon.
Fontos megjegyezni, hogy egy ideális világban a controller semmit sem kellene, hogy tudjon a konkrét UI technológiáról, csak és kizárólag interfész absztrakciókon keresztül kommunikál a view-al. Ezért került az interfészre a Clear és WriteLine() metódus is, ami az implementációban a konzol ugyan ezen nevű metódusait hívja. A Read metódus az adat olvasásért felelős.
Ha több kontrollerrel és nézettel rendelkezne az alkalmazás, akkor ezt a három metódust érdemes lenne kiszervezni egy közös interfész mögé, mivel ezen műveletek alap építő kövek és ha szorosan nézzük a view feladatát, akkor csak a ShowTaskList metódus valósít meg olyan logikát, ami specifikus és az adott nézethez tartozik.
A Read metódus egy IConvertible típust implementáló T típussal tér vissza. Az IConvertible osztály valósítja meg a különböző alap típusok közötti konverziót a .NET-en belül. Általában ezen konverziók meghívása közvetett módon szokott történni a Convert osztály metódusaival. Ezen implementáció generikusan lehetővé teszi, hogy egy szöveget beolvasva megfelelő formátumba konvertált adatot kapjunk.
A TaskView implementálja a view absztrakciós interfészét, a Program osztályunkban lévő Main metódus pedig összeállítja a függőségeket és futtatja az alkalmazásunkat.
A bemutatott példa egy picit állatorvosi ló olyan szempontból, hogy a modell rétegünk igencsak vékony, illetve nincsenek mentve az adatok semmilyen formában. Ennek az oka az, hogy egy valós életbeli architektúrában az MVC és az MVVM Modell rétege további rétegekre bomlik tovább. Ha adatbázis kapcsolatunk lenne, akkor bejönne még két réteg a kódunkba: A data access layer és maga az adatbázis. A data access layer felelős azért, hogy az adatok belekerüljenek az adatbázisba. Ez lehet egyszerű metódusok kollekciója is, vagy egy specifikus minta, amit erre találtak ki.