C# Corner
The Template Method Pattern in the .NET Framework
Learn how to process CSV files using the Template Method Pattern in a sample C# application.
The Template Pattern is a common software design pattern that allows the steps of an algorithm to be overridden by the implementing class without affecting its structure. Today, I'll go over how to put the Template Pattern to good use in a sample C# application.
The Template Method Pattern contains two core components: an abstract class and a concrete class. The abstract class defines the steps that may be overridden, as well as the structure of the algorithm. The concrete class implements the needed steps, customized for the client's needs.
A good use case for the Template Method Pattern is data file processing. First you need to read an input file into a string. Then you need to process the data accordingly; finally, you need to output the processed data file to disk. The first and last steps will be the same, whereas the middle step will change. The Template Method Pattern allows you to define the structure, and let the deriving class handle the data format step that changes depending on the requirements of the client.
The sample application will allow a user to calculate the mean or total for columns within a CSV formatted file and export the result to a new file. To get started, create a new C# Windows Store App within Visual Studio 2012 or 2013. Next, add a new abstract class named CsvDataFileProcessor. Next, add using statements for the System.Threading.Tasks and Windows.Storage namespaces:
using System.Threading.Tasks;
using Windows.Storage;
Then I add the LoadFile method that asynchronously reads a text file:
private async Task<string> LoadFile(StorageFile file)
{
return await FileIO.ReadTextAsync(file);
}
Next, I declare the ProcessData abstract method that formats the read-in text file in a derived class:
protected abstract string ProcessData(string content);
Last I define the ExportFile method, which calls LoadFile to load a file, then ProcessData to process its data and write out the formatted text to the given output storage file:
public async void ExportFile(StorageFile inputFile, StorageFile outputFile)
{
string content = await LoadFile(inputFile);
string formattedData = ProcessData(content);
await FileIO.WriteTextAsync(outputFile, formattedData);
}
Here's the completed CsvDataFileProcessor class implementation.
using System;
using System.Threading.Tasks;
using Windows.Storage;
namespace VSMTemplateMethodPattern
{
public abstract class CsvDataFileProcessor
{
private async Task<string> LoadFile(StorageFile file)
{
return await FileIO.ReadTextAsync(file);
}
protected abstract string ProcessData(string content);
public async void ExportFile(StorageFile inputFile, StorageFile outputFile)
{
string content = await LoadFile(inputFile);
string formattedData = ProcessData(content);
await FileIO.WriteTextAsync(outputFile, formattedData);
}
}
}
It's now time to implement the TotalDataFileProcessor class. It applies the CSvDataFileProcessor abstract class's ProcessData method to calculate the totals for each column, for a given CSV file's content. First I get the column names from the first row and add them to a StringBuilder:
var sb = new StringBuilder();
var rows = content.Split('\n');
var columnNames = rows.First().Split(',');
foreach (var columnName in columnNames)
{
sb.Append(columnName.Replace("\r", string.Empty).Replace("\n", string.Empty));
sb.Append(',');
}
sb.AppendLine();
Then I add a new line and initialize a dictionary with an integer key and double value to store the totals per column index:
sb.AppendLine();
var totals = new Dictionary<int, double>();
rows = rows.Skip(1).ToArray();
Next I loop over the data rows and columns and store a running total per column into the totals dictionary:
foreach (var row in rows)
{
int columnIndex = 0;
var columnData = row.Split(',');
foreach (var datum in columnData)
{
if (!totals.ContainsKey(columnIndex))
{
totals.Add(columnIndex, double.Parse(datum));
}
else
{
totals[columnIndex] += double.Parse(datum);
}
columnIndex++;
}
}
Then I loop over the values in the totals dictionary and add them to a single comma-delimited row into the StringBuilder object, and return the created StringBuilder string:
foreach (var total in totals.Values)
{
sb.Append(total);
sb.Append(',');
}
return sb.ToString();
The completed TotalDataFileProcessor implementation looks like this:
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace VSMTemplateMethodPattern
{
public class TotalDataFileProcessor : CsvDataFileProcessor
{
protected override string ProcessData(string content)
{
var sb = new StringBuilder();
var rows = content.Split('\n');
var columnNames = rows.First().Split(',');
foreach (var columnName in columnNames)
{
sb.Append(columnName.Replace("\r", string.Empty).Replace("\n",string.Empty));
sb.Append(',');
}
sb.AppendLine();
var totals = new Dictionary<int, double>();
rows = rows.Skip(1).ToArray();
foreach (var row in rows)
{
int columnIndex = 0;
var columnData = row.Split(',');
foreach (var datum in columnData)
{
if (!totals.ContainsKey(columnIndex))
{
totals.Add(columnIndex, double.Parse(datum));
}
else
{
totals[columnIndex] += double.Parse(datum);
}
columnIndex++;
}
}
foreach (var total in totals.Values)
{
sb.Append(total);
sb.Append(',');
}
return sb.ToString();
}
}
}
Next, I add the MeanDataFileProcessor, which is almost the same as the TotalDataFileProcessor. The difference is that after I have the totals, I calculate the mean by diving the total by the number of data rows:
foreach (var total in totals.Values)
{
double mean = total/totals.Keys.Count;
sb.Append(mean);
sb.Append(',');
}
Here's the completed MeanDataFileProcessor class.
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace VSMTemplateMethodPattern
{
public class MeanDataFileProcessor : CsvDataFileProcessor
{
protected override string ProcessData(string content)
{
var sb = new StringBuilder();
var rows = content.Split('\n');
var columnNames = rows.First().Split(',');
foreach (var columnName in columnNames)
{
sb.Append(columnName.Replace("\r", string.Empty).Replace("\n", string.Empty));
sb.Append(',');
}
sb.AppendLine();
var totals = new Dictionary<int, double>();
rows = rows.Skip(1).ToArray();
foreach (var row in rows)
{
int columnIndex = 0;
var columnData = row.Split(',');
foreach (var datum in columnData)
{
if (!totals.ContainsKey(columnIndex))
{
totals.Add(columnIndex, double.Parse(datum));
}
else
{
totals[columnIndex] += double.Parse(datum);
}
columnIndex++;
}
}
foreach (var total in totals.Values)
{
double mean = total/totals.Keys.Count;
sb.Append(mean);
sb.Append(',');
}
return sb.ToString();
}
}
}
Now that the template method classes are created, let's put them to good use. The next step is to get the view models ready for the XAML view. Create a new ViewModels directory in your project, then add the AlgorithmType enum file below. The AlgorithmType enum has two values -- Mean and Total -- which correspond to the template method classes, respectively.
namespace VSMTemplateMethodPattern.ViewModel
{
public enum AlgorithmType
{
Mean,
Total
};
}
Next, I add the AlgorithmTypeViewModel class that will be used to populate a ComboBox in the client application, to allow the user to select their algorithm of choice. The class contains a Text string type property and a Value Algorithm type property:
namespace VSMTemplateMethodPattern.ViewModel
{
public class AlgorithmTypeViewModel
{
public string Text { get; set; }
public AlgorithmType Value { get; set; }
}
}
Now it's time to set up the UI. Open up your MainPage.xaml file and copy the root Grid element here:
<Page
x:Class="VSMTemplateMethodPattern.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VSMTemplateMethodPattern"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Margin="20">
<TextBlock>Algorithm</TextBlock>
<ComboBox Name="AlgorithmTypes" ItemsSource="{Binding}" DisplayMemberPath="Text"
SelectedValuePath="Value" SelectionChanged="Algorithm_SelectionChanged"></ComboBox>
<TextBlock>Input File</TextBlock>
<TextBox Name="InputFileName" IsEnabled="False"></TextBox>
<Button Name="InputFile" Click="InputFile_Click">...</Button>
<TextBlock>Output File</TextBlock>
<TextBox Name="OutputFileName" IsEnabled="False"></TextBox>
<Button Name="OutputFile" Click="OutputFile_Click">...</Button>
<Button Name="ProcessFile" Click="ProcessFile_Click">Process File</Button>
<TextBlock Name="Status" Foreground="Green"></TextBlock>
</StackPanel>
</Grid>
</Page>
Now for the final step to wire up everything in the MainPage class. First, add using statements for the Windows.Storage, Windows.Storage.Pickers, and your ViewModel namespace:
using VSMTemplateMethodPattern.ViewModel;
using Windows.Storage;
using Windows Storage.Pickers;
Next, I add class variables to store the CsvDataFileProcessor, and the input and output selected files:
private CsvDataFileProcessor _csvDataFileProcessor;
private StorageFile _inputFIle;
private StorageFile _outputFile;
In the OnNavigatedTo event handler, I call the LoadAlgorithmTypes method to populate the AlgorithmTypes ComboBox:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadAlgorithmTypes();
}
Next I add the LoadAlgorithmTypes method. The LoadAlgorithmsTypes method simply creates a generic list of AlgorithmTypeViewModel with the Mean and Total AlgorithmType values, and binds it to the AlgorithmTypes ComboBox element:
private void LoadAlgorithmTypes()
{
var algorithms = new List<AlgorithmTypeViewModel>()
{
new AlgorithmTypeViewModel {Text = AlgorithmType.Mean.ToString(), Value = AlgorithmType.Mean},
new AlgorithmTypeViewModel {Text = AlgorithmType.Total.ToString(), Value = AlgorithmType.Total}
};
AlgorithmTypes.ItemsSource = algorithms;
AlgorithmTypes.SelectedIndex = 0;
}
The InputFile_Click method handles the InputFile button's click event. In the method I create a new FileOpenPicker and allow the user to select a .csv file. Once the file's picked, I store its path in the InputFileName TextBox and set the StorageFile to the _inutFile variable:
private async void InputFile_Click(object sender, RoutedEventArgs e)
{
var filePicker = new FileOpenPicker();
filePicker.FileTypeFilter.Add(".csv");
var file = await filePicker.PickSingleFileAsync();
if (file != null)
{
InputFileName.Text = file.Path;
_inputFIle = file;
}
}
The OutputFile_Click method handles the OutputFile button's Click event. In the method, I create a new FileSavePicker that allows the user to create a new .csv file. Once the file's been created, I store its path into the OutputFileName TextBox and store it into the _outputFile variable:
private async void OutputFile_Click(object sender, RoutedEventArgs e)
private async void OutputFile_Click(object sender, RoutedEventArgs e)
{
var filePicker = new FileSavePicker();
filePicker.FileTypeChoices.Add("Csv Files", new List<string>() { ".csv"});
var file = await filePicker.PickSaveFileAsync();
if (file != null)
{
OutputFileName.Text = file.Path;
_outputFile = file;
}
}
The ProcessFile_Click method handles the ProcessFile Click event handler. In the method, I call the ExportFile method on the _csvDataFileProcessor, if there are both an input and output file selected. Then I set the Status TextBlock to display "Processing Completed!":
private void ProcessFile_Click(object sender, RoutedEventArgs e)
{
Status.Text = string.Empty;
if (!String.IsNullOrWhiteSpace(InputFileName.Text) && !String.IsNullOrWhiteSpace(OutputFileName.Text))
{
_csvDataFileProcessor.ExportFile(_inputFIle, _outputFile);
}
Status.Text = "Processing Completed!";
}
The Algorithm_SelectionChanged method handles the SelectionChanged event on the Algorithm ComboBox. In the method, I get the selected algorithm type from the ComboBox and create either a MeanDataFileProcessor or a new TotalDataFileProcessor as needed:
private void Algorithm_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var algorithmType = (AlgorithmType)AlgorithmTypes.SelectedValue;
switch (algorithmType)
{
case AlgorithmType.Mean:
_csvDataFileProcessor = new MeanDataFileProcessor();
break;
case AlgorithmType.Total:
_csvDataFileProcessor = new TotalDataFileProcessor();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
The completed MainPage class implementation:
using System;
using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using VSMTemplateMethodPattern.ViewModel;
using Windows.Storage;
using Windows.Storage.Pickers;
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238
namespace VSMTemplateMethodPattern
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private CsvDataFileProcessor _csvDataFileProcessor;
private StorageFile _inputFIle;
private StorageFile _outputFile;
public MainPage()
{
this.InitializeComponent();
}
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadAlgorithmTypes();
}
private void LoadAlgorithmTypes()
{
var algorithms = new List<AlgorithmTypeViewModel>()
{
new AlgorithmTypeViewModel {Text = AlgorithmType.Mean.ToString(), Value = AlgorithmType.Mean},
new AlgorithmTypeViewModel {Text = AlgorithmType.Total.ToString(), Value = AlgorithmType.Total}
};
AlgorithmTypes.ItemsSource = algorithms;
AlgorithmTypes.SelectedIndex = 0;
}
private async void InputFile_Click(object sender, RoutedEventArgs e)
{
var filePicker = new FileOpenPicker();
filePicker.FileTypeFilter.Add(".csv");
var file = await filePicker.PickSingleFileAsync();
if (file != null)
{
InputFileName.Text = file.Path;
_inputFIle = file;
}
}
private async void OutputFile_Click(object sender, RoutedEventArgs e)
{
var filePicker = new FileSavePicker();
filePicker.FileTypeChoices.Add("Csv Files", new List<string>() { ".csv"});
var file = await filePicker.PickSaveFileAsync();
if (file != null)
{
OutputFileName.Text = file.Path;
_outputFile = file;
}
}
private void ProcessFile_Click(object sender, RoutedEventArgs e)
{
Status.Text = string.Empty;
if (!String.IsNullOrWhiteSpace(InputFileName.Text) && !String.IsNullOrWhiteSpace(OutputFileName.Text))
{
_csvDataFileProcessor.ExportFile(_inputFIle, _outputFile);
}
Status.Text = "Processing Completed!";
}
private void Algorithm_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var algorithmType = (AlgorithmType)AlgorithmTypes.SelectedValue;
switch (algorithmType)
{
case AlgorithmType.Mean:
_csvDataFileProcessor = new MeanDataFileProcessor();
break;
case AlgorithmType.Total:
_csvDataFileProcessor = new TotalDataFileProcessor();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
The application is now complete, and you should be able to process a given CSV file, as seen in Figure 1. The code download contains a sample CSV file to use in its Content folder.
Today you've seen how to implement the Template Method Pattern in a sample Windows Store App. The Template Method Pattern is best suited for handling the implementation of a method that has steps in a defined structure, that need to change depending on the client's needs.