Table of Contents
- 2.1 Command Pattern Features
- 2.2 The four actors of the pattern
- 2.3 The command and its data
- 2.4 The Command Pattern in visual practice
- 2.5 Advantages: Undo/Redo and replay of commands
- 3.1 Starting point: application without pattern
- 3.2 Demo project structure
- 3.3 ICommand interface
- 3.4 The CommandManager
- 4.1 Product model
- 4.2 The repositories
- 4.3 AddToCartCommand
- 4.4 ChangeQuantityCommand
- 4.5 RemoveFromCartCommand
- 4.6 RemoveAllFromCartCommand
- 4.7 Use in console application
- 5.1 The ICommand interface of WPF (System.Windows.Input)
- 5.2 The RelayCommand
- 5.3 MVVM Architecture and ViewModels
- 5.4 ViewModelBase and INotifyPropertyChanged
- 5.5 ShoppingCartViewModel
- 5.6 ProductViewModel
- 5.7 MainWindow and DataContext
- Advantages and disadvantages of Command Pattern
- Summary and conclusion
- Complete structure of the demo project
1. Course Overview
This course covers the Command Pattern, a behavioral design pattern that can be applied in many types of C# applications. The course is aimed at developers who wish to understand and master this pattern.
Prerequisites: Knowledge of C# syntax and how to compile and run .NET applications.
What you will learn:
- What the Command Pattern is and its characteristics
- The benefits and trade-offs when using the Command Pattern
- How to implement the Command Pattern in new and existing solutions
- How to identify and leverage existing implementations
Expected result: Be able to build more reliable, more extensible and more testable C# applications using the Command Pattern.
2. The Command Pattern
2.1 Characteristics of the Command Pattern
The Command Pattern is a behavioral design pattern that can be applied in many different types of applications. If you’ve programmed in C# for a while, you’ve probably encountered this pattern before, especially in WPF, Xamarin or Silverlight.
2.2 The four actors of the pattern
The Command Pattern is commonly recognized by these four distinct elements:
| Actor | Role | Concrete example |
|---|---|---|
| Command | Contains the instructions and references to the elements necessary for its execution | AddToCartCommand |
| Receiver | What the command will execute (the target) | The shopping cart (ShoppingCart) |
| Summon | What is used to execute commands; keeps track of executed commands | ShoppingCartCommandManager |
| Customer | Decides which command to schedule for execution | The button in the user interface |
Execution flow:
Client --> Invoker --> Command --> Receiver
(bouton) (CommandManager) (AddToCartCommand) (ShoppingCart)
The client clicks a button, which tells the invoker to schedule an AddToCartCommand command. When this command is executed, it communicates with the receiver (the shopping cart).
2.3 The command and its data
Basic principle: The order object contains all the data necessary to process the request, whether immediately or at a later time.
This implies that:
- The order can be executed immediately as soon as the customer schedules it
- All commands can be scheduled to run later in the application lifetime
An AddToCartCommand will contain for example:
- A reference to the product to add to the cart
- A reference to the shopping cart (ShoppingCart)
- A way to check stock availability
So the command has everything it needs to run, including a mechanism to check if it can be executed (CanExecute). If stock is exhausted, the order can no longer be fulfilled.
2.4 The Command Pattern in visual practice
In an example WPF shopping cart application, each button in the interface invokes a command:
AddToCartCommand— adds product to cartChangeQuantityCommand— changes the quantity of a product- Commands for checkout, item deletion, etc.
Visual behavior related to CanExecute: Buttons are automatically disabled when CanExecute() returns false:
- Add buttons are disabled for out of stock products
- Checkout button is green only when items are in the cart
- Quantity increase button is disabled if stock is out of stock
2.5 Advantages: Undo/Redo and replay of commands
Since the CommandManager can keep track of executed commands, the Command Pattern easily allows the introduction of Undo and Redo functionality.
Replay use case: If an application crashes and restarts, we can have a list of commands executed before the crash, re-execute them in the same order, and thus find a state similar to that before the crash.
3. Implementing the Command Pattern
3.1 Starting point: application without pattern
Before the introduction of the Command Pattern, the console application code interacts directly with repositories:
// Avant refactoring : interactions directes avec les repositories
var shoppingCartRepository = new ShoppingCartRepository();
var productsRepository = new ProductsRepository();
var product = productsRepository.FindBy("SM7B");
// Ajout direct au panier - sans pattern
shoppingCartRepository.Add(product);
shoppingCartRepository.IncreaseQuantity(product.ArticleId);
// etc.
Problem: The application knows too much about repositories. We want to introduce a layer that takes care of the instructions to be executed, instructions that can be reused in different types of applications.
3.2 Structure of the demo project
The demo project is a Visual Studio 2019 solution composed of several projects:
ShoppingCart.sln
├── ShoppingCart/ (Application console, netcoreapp2.2)
│ └── Program.cs
├── ShoppingCart.Business/ (Bibliothèque, netstandard2.0)
│ ├── Commands/
│ │ ├── ICommand.cs
│ │ ├── CommandManager.cs
│ │ ├── AddToCartCommand.cs
│ │ ├── ChangeQuantityCommand.cs
│ │ ├── RemoveFromCartCommand.cs
│ │ └── RemoveAllFromCartCommand.cs
│ ├── Models/
│ │ └── Product.cs
│ └── Repositories/
│ ├── ProductsRepository.cs (+ IProductRepository)
│ └── ShoppingCartRepository.cs (+ IShoppingCartRepository)
└── ShoppingCart.Windows/ (Application WPF)
├── MainWindow.xaml.cs
└── ViewModels/
├── ViewModelBase.cs
├── RelayCommand.cs
├── ShoppingCartViewModel.cs
└── ProductViewModel.cs
Important note: The ShoppingCart.Business layer targets netstandard2.0, making it compatible with any .NET platform (console, WPF, Xamarin, etc.). This is where the concrete commands live, independent of any UI framework.
3.3 The ICommand interface
The first thing to introduce is the ICommand interface. This is the contract that defines how the CommandManager will interact with commands.
Note: There are pre-existing ICommand interfaces in the .NET framework (notably in System.Windows.Input), but to avoid referencing System.Windows.Input in the business layer, we define ours.
namespace ShoppingCart.Business.Commands
{
public interface ICommand
{
void Execute();
bool CanExecute();
void Undo();
}
}
The three methods:
Execute()— executes the commandCanExecute()— checks if the command can be executed (returnstrueorfalse)Undo()— cancels the command
3.4 The CommandManager
The CommandManager is the invoker of the pattern. It is generic — it can interact with any type of command implementing ICommand, not just shopping cart commands.
Why a Stack<ICommand>?
We use a stack because it is a LIFO (Last In, First Out) data structure. This allows orders to be canceled in the correct order: the last order executed is the first canceled.
namespace ShoppingCart.Business.Commands
{
public class CommandManager
{
private Stack<ICommand> commands = new Stack<ICommand>();
public void Invoke(ICommand command)
{
if (command.CanExecute())
{
commands.Push(command);
command.Execute();
}
}
public void Undo()
{
while (commands.Count > 0)
{
var command = commands.Pop();
command.Undo();
}
}
}
}
How Invoke works:
- Checks if the command can be executed (
CanExecute) - If yes, push the command onto the stack (
Push) - Execute the command (
Execute)
How Undo works:
- Pops the most recent command (
Pop) - Call
Undo()on this command - Repeat until the battery is empty
Possible extensions (suggested exercises):
- Execute commands at a later time (deferred execution)
- Save executed commands and re-execute them when restarting the application
- Add Redo Mechanism
4. Implementation of concrete commands
4.1 The Product model
namespace ShoppingCart.Business.Models
{
public class Product
{
public string ArticleId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public Product() { }
public Product(string articleId, string name, decimal price)
{
ArticleId = articleId;
Name = name;
Price = price;
}
}
}
4.2 Repositories
The Repository Pattern (not covered in this course) abstracts data access. We work with interfaces, regardless of whether the data is in memory, in SQL Server, etc.
IProductRepository and ProductsRepository:
public interface IProductRepository
{
Product FindBy(string articleId);
int GetStockFor(string articleId);
IEnumerable<Product> All();
void DecreaseStockBy(string articleId, int amount);
void IncreaseStockBy(string articleId, int amount);
}
public class ProductsRepository : IProductRepository
{
private Dictionary<string, (Product Product, int Stock)> products { get; }
public ProductsRepository()
{
products = new Dictionary<string, (Product Product, int Stock)>();
// Données de démonstration
Add(new Product("EOSR1", "Canon EOS R", 1099), 2);
Add(new Product("EOS70D", "Canon EOS 70D", 699), 1);
Add(new Product("ATOMOSNV", "Atomos Ninja V", 799), 0); // Rupture de stock
Add(new Product("SM7B", "Shure SM7B", 399), 5);
}
public void Add(Product product, int stock)
{
products[product.ArticleId] = (product, stock);
}
public void DecreaseStockBy(string articleId, int amount)
{
if (!products.ContainsKey(articleId)) return;
products[articleId] = (products[articleId].Product, products[articleId].Stock - amount);
}
public void IncreaseStockBy(string articleId, int amount)
{
if (!products.ContainsKey(articleId)) return;
products[articleId] = (products[articleId].Product, products[articleId].Stock + amount);
}
public IEnumerable<Product> All() => products.Select(x => x.Value.Product);
public int GetStockFor(string articleId)
{
if (products.ContainsKey(articleId))
return products[articleId].Stock;
return 0;
}
public Product FindBy(string articleId)
{
if (products.ContainsKey(articleId))
return products[articleId].Product;
return null;
}
}
Note on tuples: The repository uses C# tuples named (Product Product, int Stock) as the dictionary value type. This is a modern feature of C# that improves readability without requiring a wrapper class.
IShoppingCartRepository and ShoppingCartRepository:
public interface IShoppingCartRepository
{
(Product Product, int Quantity) Get(string articleId);
IEnumerable<(Product Product, int Quantity)> All();
void Add(Product product);
void RemoveAll(string articleId);
void IncreaseQuantity(string articleId);
void DecraseQuantity(string articleId);
}
public class ShoppingCartRepository : IShoppingCartRepository
{
public Dictionary<string, (Product Product, int Quantity)> LineItems
= new Dictionary<string, (Product Product, int Quantity)>();
public IEnumerable<(Product Product, int Quantity)> All() => LineItems.Values;
public (Product Product, int Quantity) Get(string articleId)
{
if (LineItems.ContainsKey(articleId))
return LineItems[articleId];
return (default, default);
}
public void Add(Product product)
{
if (LineItems.ContainsKey(product.ArticleId))
IncreaseQuantity(product.ArticleId);
else
LineItems[product.ArticleId] = (product, 1);
}
public void DecraseQuantity(string articleId)
{
if (LineItems.ContainsKey(articleId))
{
var lineItem = LineItems[articleId];
if (lineItem.Quantity == 1)
LineItems.Remove(articleId);
else
LineItems[articleId] = (lineItem.Product, lineItem.Quantity - 1);
}
else
{
throw new KeyNotFoundException(
$"Product with article id {articleId} not in cart, please add first");
}
}
public void IncreaseQuantity(string articleId)
{
if (LineItems.ContainsKey(articleId))
{
var lineItem = LineItems[articleId];
LineItems[articleId] = (lineItem.Product, lineItem.Quantity + 1);
}
else
{
throw new KeyNotFoundException(
$"Product with article id {articleId} not in cart, please add first");
}
}
public void RemoveAll(string articleId) => LineItems.Remove(articleId);
}
4.3 AddToCartCommand
Important design principle: The author prefers that the command receive all of its parameters in the constructor, rather than receiving them via Execute or CanExecute. This makes commands easier to execute without depending on external input at runtime.
Note that the command works with interfaces (IShoppingCartRepository, IProductRepository), not with concrete implementations. This means that we can use different implementations depending on the situation (e.g. test vs production).
namespace ShoppingCart.Business.Commands
{
public class AddToCartCommand : ICommand
{
private readonly IShoppingCartRepository shoppingCartRepository;
private readonly IProductRepository productRepository;
private readonly Product product;
public AddToCartCommand(IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository,
Product product)
{
this.shoppingCartRepository = shoppingCartRepository;
this.productRepository = productRepository;
this.product = product;
}
public bool CanExecute()
{
if (product == null) return false;
// On peut ajouter au panier tant qu'il y a du stock
return productRepository.GetStockFor(product.ArticleId) > 0;
}
public void Execute()
{
if (product == null) return;
// Diminuer le stock disponible
productRepository.DecreaseStockBy(product.ArticleId, 1);
// Ajouter au panier
shoppingCartRepository.Add(product);
}
public void Undo()
{
if (product == null) return;
// Récupérer le lineItem dans le panier
var lineItem = shoppingCartRepository.Get(product.ArticleId);
// Remettre le stock
productRepository.IncreaseStockBy(product.ArticleId, lineItem.Quantity);
// Retirer tous les articles du panier
shoppingCartRepository.RemoveAll(product.ArticleId);
}
}
}
Undologic forAddToCartCommand`:
- Retrieve quantity currently in cart
- Return this quantity to stock
- Remove item from cart
4.4 ChangeQuantityCommand
This command manages both the increase and decrease of the quantity of a product in the basket. An Operation enumeration determines the behavior.
namespace ShoppingCart.Business.Commands
{
public class ChangeQuantityCommand : ICommand
{
public enum Operation
{
Increase,
Decrease
}
private readonly Operation operation;
private readonly IShoppingCartRepository shoppingCartRepository;
private readonly IProductRepository productRepository;
private readonly Product product;
public ChangeQuantityCommand(Operation operation,
IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository,
Product product)
{
this.operation = operation;
this.shoppingCartRepository = shoppingCartRepository;
this.productRepository = productRepository;
this.product = product;
}
public void Execute()
{
switch (operation)
{
case Operation.Decrease:
// Remettre 1 unité en stock, diminuer quantité dans le panier
productRepository.IncreaseStockBy(product.ArticleId, 1);
shoppingCartRepository.DecraseQuantity(product.ArticleId);
break;
case Operation.Increase:
// Retirer 1 unité du stock, augmenter quantité dans le panier
productRepository.DecreaseStockBy(product.ArticleId, 1);
shoppingCartRepository.IncreaseQuantity(product.ArticleId);
break;
}
}
public bool CanExecute()
{
switch (operation)
{
case Operation.Decrease:
// On peut diminuer tant que la quantité dans le panier n'est pas 0
return shoppingCartRepository.Get(product.ArticleId).Quantity != 0;
case Operation.Increase:
// On peut augmenter tant qu'il reste du stock
return (productRepository.GetStockFor(product.ArticleId) - 1) >= 0;
}
return false;
}
public void Undo()
{
switch (operation)
{
case Operation.Decrease:
productRepository.DecreaseStockBy(product.ArticleId, 1);
shoppingCartRepository.IncreaseQuantity(product.ArticleId);
break;
case Operation.Increase:
productRepository.IncreaseStockBy(product.ArticleId, 1);
shoppingCartRepository.DecraseQuantity(product.ArticleId);
break;
}
}
}
}
Execute/Undo symmetry: The Undo operation is the exact opposite of Execute. If Execute increased the quantity in the cart, Undo decreases it (and vice versa for the stock).
4.5 RemoveFromCartCommand
This command removes all items of a specific product from the cart and restores inventory.
namespace ShoppingCart.Business.Commands
{
public class RemoveFromCartCommand : ICommand
{
private readonly IShoppingCartRepository shoppingCartRepository;
private readonly IProductRepository productRepository;
private readonly Product product;
public RemoveFromCartCommand(IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository,
Product product)
{
this.shoppingCartRepository = shoppingCartRepository;
this.productRepository = productRepository;
this.product = product;
}
public bool CanExecute()
{
if (product == null) return false;
return shoppingCartRepository.Get(product.ArticleId).Quantity > 0;
}
public void Execute()
{
if (product == null) return;
var lineItem = shoppingCartRepository.Get(product.ArticleId);
// Remettre toute la quantité en stock
productRepository.IncreaseStockBy(product.ArticleId, lineItem.Quantity);
// Supprimer du panier
shoppingCartRepository.RemoveAll(product.ArticleId);
}
public void Undo()
{
// Undo non implémenté intentionnellement
// La commande ne connaît pas la quantité qui était dans le panier avant la suppression
throw new NotImplementedException();
}
}
}
Note on Undo not implemented: The order does not know how many items were in the cart before deletion. To implement Undo, one would have to store the quantity (lineItem.Quantity) in an instance variable before deletion, then reuse it in Undo. This is an exercise left to the reader.
4.6 RemoveAllFromCartCommand
This command completely empties the cart and restores all stock.
namespace ShoppingCart.Business.Commands
{
public class RemoveAllFromCartCommand : ICommand
{
private readonly IShoppingCartRepository shoppingCartRepository;
private readonly IProductRepository productRepository;
public RemoveAllFromCartCommand(IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository)
{
this.shoppingCartRepository = shoppingCartRepository;
this.productRepository = productRepository;
}
public bool CanExecute()
{
// Peut vider le panier seulement s'il n'est pas déjà vide
return shoppingCartRepository.All().Any();
}
public void Execute()
{
// Copie locale importante : évite de modifier la collection en l'itérant
var items = shoppingCartRepository.All().ToArray();
foreach (var lineItem in items)
{
productRepository.IncreaseStockBy(lineItem.Product.ArticleId, lineItem.Quantity);
shoppingCartRepository.RemoveAll(lineItem.Product.ArticleId);
}
}
public void Undo()
{
throw new NotImplementedException();
}
}
}
Technical note: The .ToArray() call creates a local copy of the collection before iterating over it. This is essential because we modify the source collection (shoppingCartRepository) during the iteration — without this copy, we would get an InvalidOperationException.
4.7 Usage in console application
Here is the complete Program.cs of the console application, illustrating the use of the CommandManager and concrete commands:
using ShoppingCart.Business.Commands;
using ShoppingCart.Business.Repositories;
using System;
namespace ShoppingCart
{
class Program
{
static void Main(string[] args)
{
var shoppingCartRepository = new ShoppingCartRepository();
var productsRepository = new ProductsRepository();
// Trouver le produit SM7B (Shure microphone)
var product = productsRepository.FindBy("SM7B");
// Créer les commandes concrètes
var addToCartCommand = new AddToCartCommand(
shoppingCartRepository,
productsRepository,
product);
var increaseQuantityCommand = new ChangeQuantityCommand(
ChangeQuantityCommand.Operation.Increase,
shoppingCartRepository,
productsRepository,
product);
// Utiliser le CommandManager pour exécuter les commandes
var manager = new CommandManager();
manager.Invoke(addToCartCommand); // Ajoute 1 SM7B
manager.Invoke(increaseQuantityCommand); // Quantité = 2
manager.Invoke(increaseQuantityCommand); // Quantité = 3
manager.Invoke(increaseQuantityCommand); // Quantité = 4
manager.Invoke(increaseQuantityCommand); // Quantité = 5
// Afficher le panier (5 SM7B à $399 = $1995)
PrintCart(shoppingCartRepository);
// Annuler TOUTES les commandes
manager.Undo();
// Afficher le panier (maintenant vide, total = $0)
PrintCart(shoppingCartRepository);
}
static void PrintCart(ShoppingCartRepository shoppingCartRepository)
{
var totalPrice = 0m;
foreach (var lineItem in shoppingCartRepository.LineItems)
{
var price = lineItem.Value.Product.Price * lineItem.Value.Quantity;
Console.WriteLine($"{lineItem.Key} " +
$"${lineItem.Value.Product.Price} x {lineItem.Value.Quantity} = ${price}");
totalPrice += price;
}
Console.WriteLine($"Total price:\t${totalPrice}");
}
}
}
Expected result at execution:
SM7B $399 x 5 = $1995
Total price: $1995
Total price: $0
The first release shows 5 SM7Bs at $399 each = $1995 total. After manager.Undo(), the cart is empty and the total is $0.
Key benefit: The console application no longer needs to know how to interact with repositories. This is the command that communicates with the receiver. The console just creates commands and passes them to the invoker.
5. The Command Pattern in action: WPF integration
5.1 The WPF ICommand interface (System.Windows.Input)
WPF has its own implementation of the ICommand interface in System.Windows.Input:
namespace System.Windows.Input
{
public interface ICommand
{
void Execute(object parameter);
bool CanExecute(object parameter);
event EventHandler CanExecuteChanged;
}
}
Differences with our custom ICommand:
ExecuteandCanExecutetake anobjectparameter (which is not used in this course)- There is a
CanExecuteChangedevent that notifies the UI framework whenCanExecutechanges state
This is why this interface is not suitable for the business layer: it is specific to WPF and would force a UI dependency in the business layer.
5.2 The RelayCommand
The RelayCommand is a bridge between the WPF ICommand and our concrete business layer commands. It allows you to combine the two implementations.
Principle: The RelayCommand receives Action (for Execute) and Func<bool> (for CanExecute) as a constructor. These delegates call out our concrete orders.
using System;
using System.Windows.Input;
namespace ShoppingCart.Windows.ViewModels
{
public class RelayCommand : System.Windows.Input.ICommand
{
private readonly Action execute;
private readonly Func<bool> canExecute;
public RelayCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute?.Invoke() ?? false;
}
public void Execute(object parameter)
{
execute?.Invoke();
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}
}
CanExecuteChanged and CommandManager.RequerySuggested: WPF uses its own CommandManager (different from ours) to automatically re-evaluate CanExecute on all commands during certain UI events (like keyboard focus). By subscribing to RequerySuggested, the buttons update automatically.
Null-conditional operators:
canExecute?.Invoke()— callsInvoke()only ifcanExecuteis not null?? false— returnsfalseif the result is null
5.3 MVVM and ViewModels architecture
The WPF application uses the MVVM (Model-View-ViewModel) pattern:
- Model:
Product, repositories - View:
MainWindow.xaml(the XAML interface) - ViewModel:
ShoppingCartViewModel,ProductViewModel(contains presentation logic and controls)
The data binding
5.4 ViewModelBase and INotifyPropertyChanged
using System.ComponentModel;
namespace ShoppingCart.Windows.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyRaised(string propertyname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
}
}
INotifyPropertyChanged allows WPF to detect property changes and update the UI accordingly. When OnPropertyRaised is called with the name of a property, WPF knows to reread that property and refresh the controls related to it.
5.5 ShoppingCartViewModel
This ViewModel exposes orders and collections for the main cart view:
using ShoppingCart.Business.Commands;
using ShoppingCart.Business.Repositories;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
namespace ShoppingCart.Windows.ViewModels
{
public class ShoppingCartViewModel : ViewModelBase
{
private readonly IShoppingCartRepository shoppingCartRepository;
private readonly IProductRepository productRepository;
// Commandes exposées à la vue via data binding
public System.Windows.Input.ICommand RemoveAllFromCartCommand { get; private set; }
public System.Windows.Input.ICommand CheckoutCommand { get; private set; }
// Collections pour data binding
public ObservableCollection<ProductViewModel> Products { get; private set; }
public ObservableCollection<ProductViewModel> LineItems { get; private set; }
public ShoppingCartViewModel(IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository)
{
this.shoppingCartRepository = shoppingCartRepository;
this.productRepository = productRepository;
}
public void InitializeViewModel()
{
// Commande concrète de la couche business
var removeAllFromCartCommand =
new RemoveAllFromCartCommand(shoppingCartRepository, productRepository);
// RelayCommand qui enveloppe la commande business
RemoveAllFromCartCommand = new RelayCommand(
execute: () =>
{
removeAllFromCartCommand.Execute();
Refresh(); // Mettre à jour l'UI
},
canExecute: () => removeAllFromCartCommand.CanExecute()
);
// Commande de caisse
CheckoutCommand = new RelayCommand(
execute: () => {
var total = LineItems.Sum(x => x.Product.Price * x.Quantity);
MessageBox.Show($"Shopping cart total: ${total}");
},
canExecute: () => LineItems.Any() // Actif seulement si le panier n'est pas vide
);
Refresh();
}
public void Refresh()
{
// Recréer les ViewModels à partir des données actuelles
var products = productRepository
.All()
.Select(product => new ProductViewModel(this,
shoppingCartRepository,
productRepository,
product));
var lineItems = shoppingCartRepository
.All()
.Select(x => new ProductViewModel(this,
shoppingCartRepository,
productRepository,
x.Product,
x.Quantity));
Products = new ObservableCollection<ProductViewModel>(products);
LineItems = new ObservableCollection<ProductViewModel>(lineItems);
// Notifier WPF que ces propriétés ont changé
OnPropertyRaised(nameof(Products));
OnPropertyRaised(nameof(LineItems));
}
}
}
Important points:
- The ViewModel does not use the custom
CommandManagerhere, because we don’t want Undo functionality at this level. We call the business order methods directly. - After each command execution,
Refresh()is called to recreate the collections and notify WPF. ObservableCollection<T>is used because WPF can automatically subscribe to it to detect changes.
5.6 ProductViewModel
This ViewModel encapsulates all commands related to an individual product:
using ShoppingCart.Business.Commands;
using ShoppingCart.Business.Models;
using ShoppingCart.Business.Repositories;
namespace ShoppingCart.Windows.ViewModels
{
public class ProductViewModel
{
public Product Product { get; set; }
public int Quantity { get; set; }
// Commandes exposées pour le data binding XAML
public System.Windows.Input.ICommand AddToCartCommand { get; private set; }
public System.Windows.Input.ICommand IncreaseQuantityCommand { get; private set; }
public System.Windows.Input.ICommand DecreaseQuantityCommand { get; private set; }
public System.Windows.Input.ICommand RemoveFromCartCommand { get; private set; }
public ProductViewModel(ShoppingCartViewModel shoppingCartViewModel,
IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository,
Product product,
int quantity = 0)
{
Product = product;
Quantity = quantity;
// Créer les commandes concrètes de la couche business
var addToCartCommand =
new AddToCartCommand(shoppingCartRepository, productRepository, product);
var increaseQuantityCommand =
new ChangeQuantityCommand(ChangeQuantityCommand.Operation.Increase,
shoppingCartRepository, productRepository, product);
var decreaseQuantityCommand =
new ChangeQuantityCommand(ChangeQuantityCommand.Operation.Decrease,
shoppingCartRepository, productRepository, product);
var removeFromCartCommand =
new RemoveFromCartCommand(shoppingCartRepository, productRepository, product);
// Envelopper chaque commande dans un RelayCommand pour l'UI WPF
AddToCartCommand = new RelayCommand(
execute: () => {
addToCartCommand.Execute();
shoppingCartViewModel.Refresh();
},
canExecute: () => addToCartCommand.CanExecute());
IncreaseQuantityCommand = new RelayCommand(
execute: () => {
increaseQuantityCommand.Execute();
shoppingCartViewModel.Refresh();
},
canExecute: () => increaseQuantityCommand.CanExecute());
DecreaseQuantityCommand = new RelayCommand(
execute: () => {
decreaseQuantityCommand.Execute();
shoppingCartViewModel.Refresh();
},
canExecute: () => decreaseQuantityCommand.CanExecute());
RemoveFromCartCommand = new RelayCommand(
execute: () => {
removeFromCartCommand.Execute();
shoppingCartViewModel.Refresh();
},
canExecute: () => removeFromCartCommand.CanExecute());
}
}
}
5.7 MainWindow and DataContext
using ShoppingCart.Business.Repositories;
using ShoppingCart.Windows.ViewModels;
using System.Windows;
namespace ShoppingCart.Windows
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Créer le ViewModel avec les repositories
var viewModel = new ShoppingCartViewModel(
new ShoppingCartRepository(),
new ProductsRepository());
viewModel.InitializeViewModel();
// Lier le ViewModel à la fenêtre (data binding XAML)
DataContext = viewModel;
}
}
}
DataContext: By assigning the ViewModel to the window’s DataContext, all child XAML controls can automatically access the ViewModel’s properties and commands via data binding.
WPF application behavior:
- WPF uses its own internal
CommandManagerto automatically evaluateCanExecuteon all commands during UI interactions - When
CanExecute()returnsfalse, the button is automatically disabled - When an out of stock item is removed from the cart, the add button automatically reactivates
6. Advantages and disadvantages of Command Pattern
Advantages
| Advantage | Description |
|---|---|
| Separation of responsibilities | The UI layer no longer needs to know the interaction logic with repositories or data layers |
| Increased testability | Commands are independently testable units, with mockable interfaces |
| Extensibility | Adding a new functionality means creating a new command without modifying existing commands (Open/Closed principle) |
| Undo/Redo | The command stack makes it easy to implement undo and redo |
| Command replay | Commands can be persisted and replayed (e.g. after a crash) |
| Delayed execution | Commands can be scheduled for later execution |
| Interoperability | Business commands can be used in different UI contexts (WPF, console, Xamarin) thanks to interface abstraction |
Disadvantages
| Disadvantage | Description |
|---|---|
| Increased complexity | Adds an extra layer in architecture |
| More classes | Each operation requires a dedicated command class |
| Overhead | For small applications, the pattern may be disproportionate |
Recommendation: For small applications, the Command Pattern is not necessarily justified. In large line-of-business applications, it makes developers’ lives much easier.
7. Summary and conclusion
This course covered the essential aspects of the Command Pattern in C#:
What we learned
- The four actors of the Command Pattern: Command, Receiver, Invoker, Client
- The command contains everything it needs to run (parameters in the constructor)
- The ICommand interface with
Execute(),CanExecute(), andUndo() - The CommandManager as an invoker using a
Stack<ICommand>for LIFO - Concrete commands:
AddToCartCommand,ChangeQuantityCommand,RemoveFromCartCommand,RemoveAllFromCartCommand - WPF integration via the
RelayCommandwhich bridges betweenSystem.Windows.Input.ICommandand our business commands - MVVM architecture with
ViewModelBase,INotifyPropertyChanged,ObservableCollection
Suggested exercises
- Introduce the
CommandManagerinto the WPF application and add an Undo button - Add Redo Button
- Implement persistence of commands to replay them on restart
- Implement deferred execution
- Complete implementation of
Undo()inRemoveFromCartCommandby storing the quantity before deletion
Principles applied
- Separation of Concerns: UI, ViewModel, and business commands are decoupled
- Single Responsibility Principle: each command does only one thing
- Open/Closed principle: you can add commands without modifying the existing ones
- Dependency Inversion Principle: commands depend on interfaces, not concrete implementations
- Programming to interfaces:
ICommand,IShoppingCartRepository,IProductRepository
8. Complete Demo Project Structure
ShoppingCart.sln
│
├── ShoppingCart/ [netcoreapp2.2 - Application console]
│ ├── ShoppingCart.csproj
│ └── Program.cs
│ └── Utilise : CommandManager, AddToCartCommand, ChangeQuantityCommand
│
├── ShoppingCart.Business/ [netstandard2.0 - Bibliothèque partagée]
│ ├── ShoppingCart.Business.csproj
│ ├── Commands/
│ │ ├── ICommand.cs ← Interface : Execute, CanExecute, Undo
│ │ ├── CommandManager.cs ← Invoker : Stack<ICommand>, Invoke, Undo
│ │ ├── AddToCartCommand.cs ← Ajout produit au panier
│ │ ├── ChangeQuantityCommand.cs ← Modification quantité (Increase/Decrease)
│ │ ├── RemoveFromCartCommand.cs ← Suppression d'un produit du panier
│ │ └── RemoveAllFromCartCommand.cs ← Vidage complet du panier
│ ├── Models/
│ │ └── Product.cs ← ArticleId, Name, Price
│ └── Repositories/
│ ├── ProductsRepository.cs ← ProductsRepository + IProductRepository
│ └── ShoppingCartRepository.cs ← ShoppingCartRepository + IShoppingCartRepository
│
└── ShoppingCart.Windows/ [WPF - Application Windows]
├── ShoppingCart.Windows.csproj
├── App.xaml.cs
├── MainWindow.xaml.cs ← DataContext = ShoppingCartViewModel
├── Properties/
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ └── Settings.Designer.cs
└── ViewModels/
├── ViewModelBase.cs ← INotifyPropertyChanged
├── RelayCommand.cs ← Pont entre WPF ICommand et commandes business
├── ShoppingCartViewModel.cs ← Products, LineItems, RemoveAllFromCart, Checkout
└── ProductViewModel.cs ← AddToCart, IncreaseQty, DecreaseQty, RemoveFromCart
Data flow:
MainWindow.xaml (XAML binding)
|
v
ShoppingCartViewModel / ProductViewModel (RelayCommand)
|
v
AddToCartCommand / ChangeQuantityCommand / ... (ICommand business)
|
v
IShoppingCartRepository / IProductRepository
|
v
ShoppingCartRepository / ProductsRepository (données en mémoire)
Search Terms
c-sharp · design · patterns · command · testing · architecture · c# · .net · development · pattern · advantages · application · commands · disadvantages · icommand · interface · wpf