WinUI 3.0 and C# Examples

The Power of Factories

Imagine you want to create different types of cars in a car manufacturing plant. Instead of manually building each car from scratch every time, you have a dedicated assembly line (the factory) that follows the steps and processes to create the cars.

The factory knows the rules and configurations for creating each type of car. It has the necessary machinery, tools, and workers trained in those tasks. When you need a car, you tell the factory what type of car you want, and it follows the predefined steps to construct and deliver that car to you.

Similarly, in programming, a factory is like that assembly line. It's a structured and centralized mechanism for creating objects. It has the knowledge of how to create objects based on rules, parameters, or configurations. You provide the necessary information to the factory, and it creates the object for you, ensuring consistency and reducing repetitive code.

Using a factory helps organize object creation logic in one place, making it easier to manage and maintain. It provides a reliable and standardized way to create objects, allowing for flexibility and avoiding duplicated code across different parts of the program.

Drawbacks of Not Using a Factory

In this example, the MainViewModel class attempts to create car objects directly without using a factory. However, we simulate a factory-like behavior by introducing a dictionary _carCreationStrategies that stores different creation strategies for each car type.

C#
// MainWindow.xaml.cs - View code-behind
public sealed partial class MainWindow : Window
{
	private MainViewModel _viewModel;

	public MainWindow()
	{
		InitializeComponent();

		_viewModel = new MainViewModel();
	}

	private void CreateCarButton_Click(object sender, RoutedEventArgs e)
	{
		_viewModel.CreateCar("Sedan", "Toyota");
	}
}
C#
// MainViewModel.cs - ViewModel class
public class MainViewModel : INotifyPropertyChanged
{
	private Dictionary<string, Func<Car>> _carCreationStrategies;

	public MainViewModel()
	{
		_carCreationStrategies = new Dictionary<string, Func<Car>>
		{
			{ "Sedan", () => CreateCar("Sedan", "Example Manufacturer") },
			{ "SUV", () => CreateCar("SUV", "Another Manufacturer") },
			// Add more strategies for different car types...
		};
	}

	public void CreateCar(string model, string manufacturer)
	{
		if (!_carCreationStrategies.ContainsKey(model))
			throw new ArgumentException("Invalid car type.");

		Func creationStrategy = _carCreationStrategies[model];
		Car newCar = creationStrategy.Invoke();

		// Do further operations with the created car object
		// ...
	}
}

Without a factory, the ViewModel becomes burdened with the responsibility of creating objects, which can lead to various issues:

  • Violation of Single Responsibility Principle (SRP): The ViewModel is responsible for both managing the application logic and constructing objects. This leads to a lack of separation of concerns.
  • Code Duplication: If multiple methods or parts of the ViewModel need to create cars, the object creation logic needs to be repeated, resulting in duplicated code.
  • Maintenance Challenges: If the object creation process needs to change, such as adding additional steps or rules, every place in the ViewModel where objects are created would require modification.
  • Testability Issues: It becomes more challenging to write unit tests for the ViewModel as object creation is tightly coupled to the ViewModel logic.

Using a factory helps address these issues by encapsulating the object creation process, providing a dedicated place for construction logic, improving code organization, promoting code reuse, and enabling easier testing and maintainability.

Factory Pattern in WinUI 3 MVVM

In this example, the Car class represents a model of a car. The CarFactory class acts as the factory responsible for creating instances of Car objects based on specific rules or configurations. The MainViewModel class serves as the ViewModel, which uses the factory to create cars. The MainWindow class represents the View and binds to the ViewModel.

C#
// MainWindow.xaml.cs - View code-behind
public sealed partial class MainWindow : Window
{
	private MainViewModel _viewModel;
	private CarFactory _carFactory;

	public MainWindow()
	{
		InitializeComponent();

		_carFactory = new CarFactory();
		_viewModel = new MainViewModel(_carFactory);
	}

	private void CreateCarButton_Click(object sender, RoutedEventArgs e)
	{
		_viewModel.CreateCar("Sedan", "Toyota");
	}
}

When the "Create Car" button is clicked in the UI, it triggers the CreateCar method in the ViewModel.

C#
// MainViewModel.cs - ViewModel class
public class MainViewModel : INotifyPropertyChanged
{
	private CarFactory _carFactory;

	public MainViewModel()
	{
		_carFactory = new CarFactory();
	}

	public void CreateCar()
	{
		// Use the factory to create a new car object
		Car newCar = _carFactory.CreateCar("Sedan", "Toyota");

		// Do further operations with the created car object
		// For example, update UI, store in a collection, etc.
	}
}

Inside that method, the ViewModel uses the CarFactory to create a new Car object with the specified model and manufacturer. This showcases the concept of a factory creating objects based on certain rules.

C#
// Car.cs - Model class
public class Car
{
	public string Model { get; set; }
	public string Manufacturer { get; set; }
}

Next, the CarFactory class demonstrates the benefits of using a factory for creating car objects. Centralized Creation Logic: The factory class contains complex creation logic for different car types, encapsulating the details within separate private methods (CreateSedanCar, CreateSUVCar, etc.). This allows for a cleaner and more organized code structure.

C#
// CarFactory.cs - Factory class
public class CarFactory
{
	private Dictionary<string, Func<Car>> _carCreationStrategies;

	public CarFactory()
	{
		_carCreationStrategies = new Dictionary<string, Func<Car>>
		{
			{ "Sedan", CreateSedanCar },
			{ "SUV", CreateSUVCar },
			// Add more strategies for different car types...
		};
	}

	public Car CreateCar(string carType)
	{
		if (!_carCreationStrategies.ContainsKey(carType))
			throw new ArgumentException("Invalid car type.");

		Func creationStrategy = _carCreationStrategies[carType];
		return creationStrategy.Invoke();
	}

	private Car CreateSedanCar()
	{
		// Complex logic to create a sedan car...
		return new Car
		{
			Model = "Sedan",
			Manufacturer = "Example Manufacturer",
			// Additional complex properties and methods...
		};
	}

	private Car CreateSUVCar()
	{
		// Complex logic to create an SUV car...
		return new Car
		{
			Model = "SUV",
			Manufacturer = "Another Manufacturer",
			// Additional complex properties and methods...
		};
	}

	// More private methods for creating different types of cars...
}

Flexibility and Scalability: The factory class utilizes a dictionary (_carCreationStrategies) to store different creation strategies based on the car type. This allows for easy expansion by adding more strategies for additional car types without modifying the core factory logic.

Error Handling: The factory class performs error handling by checking if the requested car type exists in the _carCreationStrategies dictionary. It throws an exception (ArgumentException) when an invalid car type is provided, ensuring that only valid car types are created.

Complexity and Realistic Scenarios: The example demonstrates that a factory can handle more complex creation scenarios by showcasing the creation of different car types with custom logic for each type. This mirrors real-world situations where objects may require intricate setup or configuration.

By utilizing a factory class, you achieve a more organized, flexible, and scalable approach to object creation. It allows for centralized creation logic, promotes code reuse, and simplifies the process of adding new creation strategies.