POO is so pretty with C#

I will take a major risk with this post but, never mind, I will write about POO today. It’s a major risk because I think that every architect has different ideas and even if I use some design patterns, I know that some architects would use different ones in my sample. Moreover, I know that in my English blog, a lot of gurus will be able to read it.


In this post, no Entity Framework (oh my god Matthieu can blog on something else [:)]), only POO.


We will start with a very easy problem to see how to make an elegant, reusable and extensible architecture to solve it.


From a file, I want to read lines, process them and write them into a new file.


At first, I want to propose two processes:


  • Characters inversion line per line
  • Line numbering

Moreover, I want to be able to combine my processes.


How to do this?


In a first step, I will define two layers:


  • UI
  • BLL

Then I will start with the Business Logic Layer. I will expose to my UI the available processes. I will return a generic collection. So I have to define a common type between all the processes. In this case, I think that the use of an interface is the best way.


What do we need in this interface?


In fact we just need a method Process with a collection of string parameter and which return a collection of string. I won’t use the file path directly because with this way my processes are independent of any file notion, so more reusable. To manage every collection type, I will use the IEnumerable<string> interface. IStringsProcess will be the following:


public interface IStringsProcess

{

    IEnumerable<string> Process(IEnumerable<string> lines);

}


The two processes I want are somewhat similar: they are applied line per line. So it can be interesting to define a base class to factorize the common code. This class will be abstract because it doesn’t correspond to a real process. This class will implement the IStringsProcess interface.


public abstract class PerStringProcessBase : IStringsProcess

{

    public IEnumerable<string> Process(IEnumerable<string> lines)

    {

        foreach (string line in lines)

            yield return Process(line);

    }

 

    protected abstract string Process(string line);

}


Now we have to code the NumberPerStringProcess and ReverseCharsPerStringProcess classes .


public class NumberPerStringProcess : PerStringProcessBase

{

    private int _cpt = 0;

 

    protected override string Process(string line)

    {

        return string.Concat((++_cpt).ToString(“000”), ” : “, line);

    }

}

 

public class ReverseCharsPerStringProcess : PerStringProcessBase

{

    protected override string Process(string line)

    {

        return new string(line.Reverse().ToArray());

    }

}


We could do without the abstract class like this:


public class NumberPerStringProcess : IStringsProcess

{

    public IEnumerable<string> Process(IEnumerable<string> lines)

    {

        return lines.Select((l, index) => string.Concat((index + 1).ToString(“000”), ” : “, l));

    }

}

 

public class ReverseCharsPerStringProcess : IStringsProcess

{

    public IEnumerable<string> Process(IEnumerable<string> lines)

    {

        return lines.Select(l => new string(l.Reverse().ToArray()));

    }

}


However for this post, oriented on POO, I will use let the base class PerStringProcessBase.


Ok we have the two processes. We now need the file management.


The read file can be big. So we have to be careful to not load all file lines in memory. In this case, the yield return is very useful. Indeed it allows to process in a “streaming” mode. So in our case, the processing will be line per line.


public static class FileUtil

{

    public static IEnumerable<string> GetLines(string filePath)

    {

        using (var sr = new StreamReader(filePath))

        {

            string line;

            while ((line = sr.ReadLine()) != null)

                yield return line;

        }

    }

 

    public static void WriteLines(string filePath, IEnumerable<string> lines)

    {

        using (var sw = new StreamWriter(filePath))

        {

            foreach (string line in lines)

                sw.WriteLine(line);

        }

    }

}

 

public static class FileProcess

{

    public static void Process(string fileInPath, string fileOutPath, IStringsProcess process)

    {

        FileUtil.WriteLines(fileOutPath, process.Process(FileUtil.GetLines(fileInPath)));

    }

}


The step by step debuging execution can feel strange if you don’t master yield return. Indeed the WriteLines method will be called before the Process method. And this one will be called before the GetLines method. It’s another effect of yield return: the execution is differed and will be executed only when the IEnumerable<T> will be iterated.


Then we will expose to UI layer the list of available processes.


public static class Processes

{

    private static List<IStringsProcess> _processes;

    public static List<IStringsProcess> ProcessesAllowed

    {

        get

        {

            if (_processes == null)

                _processes = new List<IStringsProcess>()

                    {

                        new NumberPerStringProcess(),

                        new ReverseCharsPerStringProcess()

                    };

            return _processes;

        }

    }

}


Now in the UI layer, we will use a ComboBox to choose the process.


This combo will be bound to Processes.ProcessesAllowed.


What is great with the ComboBox is the fact that the items are objects. So the item will be an IStringsProcess.


However, the first problem we can see immediately is the combo items text.


By default, the text is the result of the ToString method. So we will override this method for the two processes. To do this, we will use a resources file. Like this, our application will be localizable.


Note: for web application, the DropDownList doesn’t work like the ComboBox.


The second problem is the following: if we run twice the lines numbering without restarting the application. The line index won’t be reinitialized because we use the same instance of the NumberPerStringProcess class. To fix it, we will add a virtual method Init on PerStringProcessBase and we will override it on NumberPerStringProcess to reinitialize the line index.


public abstract class PerStringProcessBase : IStringsProcess

{

    public IEnumerable<string> Process(IEnumerable<string> lines)

    {

        Init();

        foreach (string line in lines)

            yield return Process(line);

    }

 

    protected abstract string Process(string line);

 

    protected virtual void Init()

    {

    }

}

 

public class NumberPerStringProcess : PerStringProcessBase

{

    private int _cpt;

 

    protected override string Process(string line)

    {

        return string.Concat((++_cpt).ToString(“000”), ” : “, line);

    }

 

    protected override void Init()

    {

        base.Init();

        _cpt = 0;

    }

 

    public override string ToString()

    {

        return ProcessResources.NumberPerStringProcess;

    }

}

 

public class ReverseCharsPerStringProcess : PerStringProcessBase

{

    protected override string Process(string line)

    {

        return new string(line.Reverse().ToArray());

    }

 

    public override string ToString()

    {

        return ProcessResources.ReverseCharsPerStringProcess;

    }

}


In the instruction, I wrote that I want to be able the aggregate the processes. To do this, we will use the composite design pattern.


Wikipedia advices to use this pattern in this case:


“Composite can be used when clients should ignore the difference between compositions of objects and individual objects. If programmers find that they are using multiple objects in the same way, and often have nearly identical code to handle each of them, then composite is a good choice; it is less complex in this situation to treat primitives and composites as homogeneous.”


In this implementation, we will use a class to manage all the processes. This class will itself implement the IStringsProcess interface and the implementation of this interface will easily aggregate the result of each process.


public class CompositeStringsProcess : IStringsProcess

{

    private List<IStringsProcess> _processes = new List<IStringsProcess>();

 

    public CompositeStringsProcess(IEnumerable<IStringsProcess> processes)

    {

        _processes.AddRange(processes);

    }

    public CompositeStringsProcess(params IStringsProcess[] processes)

        : this((IEnumerable<IStringsProcess>)processes)

    {

    }

 

    public List<IStringsProcess> Processes

    {

        get { return _processes; }

    }

 

    public IEnumerable<string> Process(IEnumerable<string> lines)

    {

        foreach (var process in Processes)

            lines = process.Process(lines);

        return lines;

    }

 

    public override string ToString()

    {

        var sb = new StringBuilder();

        foreach (var process in Processes)

        {

            sb.Append(” & “);

            sb.Append(process);

        }

        return sb.ToString().Substring(3);

    }

}


All the needs are now covered. However, it isn’t finished.


Indeed, there is no error management.


The problem is the following: error management has to be in Business Logic Layer but the BLL doesn’t know what it should do with errors. Show it in a MessageBox, a text in the console, an asp page, log it?


The BLL can’t know it. The BLL could throw an exception and could let the UI catch it but it’s a shame to throw an exception when it isn’t necessary.


In fact the only thing that changes is the way to show the error. So to do it, we will use a delegate.


For this, we will change the FileProcess.Process method to add a delegate parameter in addition to the files path and the process. This delegate will be used to show the errors.


Some errors can allow the user to retry (ex: the output file is locked). So we will use two delegates.


public static class FileProcess

{

    public static bool Process(string inFilePath, string outFilePath, IStringsProcess process, Action<string> showError, Func<string, bool> showErrorWithRetry)

    {

        if (!File.Exists(inFilePath))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

            while (true)

            {

                try

                {

                    FileUtil.WriteLines(outFilePath, process.Process(FileUtil.GetLines(inFilePath)));

                    return true;

                }

                catch (FileNotFoundException)

                {

                    FileNotFoundShowError(inFilePath, showError);

                }

                catch (DirectoryNotFoundException)

                {

                    DirectoryNotFoundShowError(outFilePath, showError);

                }

                catch (IOException)

                {

                    if (showErrorWithRetry(string.Format(FileErrorResources.IOException, outFilePath)))

                        continue;

                }

                catch (Exception ex)

                {

                    showError(string.Format(FileErrorResources.UnknowException, string.Concat(ex.GetType(), ” : “, ex.Message)));

                }

                break;

            }

        return false;

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.FileNotFoundException, filePath));

    }

}


Ok. It’s better but however, we aren’t finished yet. Indeed, files can be very big so the process can be long. In this case, the process can freeze the UI.


So we will use a second thread to run the processes. To make it, I will use a BackgroundWorker. I know that the purists don’t like it because it implies to need the System.ComponentModel namespace in BLL. However, it’s so easy to use the BackgroundWorker that I will do it.


So the FileProcess class becomes the following:


public static class FileProcess

{

    public static void Process(string inFilePath, string outFilePath, IStringsProcess process,

        Action<string> showError, Func<string, bool> showErrorWithRetry, Action<bool> traitmentDone)

    {

        if (!File.Exists(inFilePath))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            var bg = new BackgroundWorker();

            bg.DoWork += (s, e) =>

            {

                e.Result = false;

                while (true)

                {

                    try

                    {

                        FileUtil.WriteLines(outFilePath, process.Process(FileUtil.GetLines(inFilePath)));

                        e.Result = true;

                    }

                    catch (FileNotFoundException)

                    {

                        FileNotFoundShowError(inFilePath, showError);

                    }

                    catch (DirectoryNotFoundException)

                    {

                        DirectoryNotFoundShowError(outFilePath, showError);

                    }

                    catch (IOException)

                    {

                        if (showErrorWithRetry(string.Format(FileErrorResources.IOException, outFilePath)))

                            continue;

                    }

                    catch (Exception ex)

                    {

                        showError(string.Format(FileErrorResources.UnknowException,

                            string.Concat(ex.GetType(), ” : “, ex.Message)));

                    }

                    break;

                }

            };

            bg.RunWorkerCompleted += (s, e) =>

            {

                if (traitmentDone != null)

                    traitmentDone((bool)e.Result);

            };

            bg.RunWorkerAsync();

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.FileNotFoundException, filePath));

    }

}


An interesting point with this is the fact the processes will be run in parallel. This is great but it can also be a problem. Imagine that we want to run a batch with many file processes. If there are some dependences between the file processes (the output file becomes an input process for another process), we have to wait. For this, we have to change again the FileProcess.Process method:


public static class FileProcess

{

    private static Dictionary<string, BackgroundWorker> _traitmentsRunning = new Dictionary<string, BackgroundWorker>();

    private static object _lockObject = new object();

    private static object _bwLockObject = new object();

 

    public static void Process(string inFilePath, string outFilePath, IStringsProcess process,

        Action<string> showError, Func<string, bool> showErrorWithRetry, Action<bool> traitmentDone)

    {

        if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            lock (_lockObject)

            {

                if (!(File.Exists(inFilePath) || _traitmentsRunning.ContainsKey(inFilePath)))

                    FileNotFoundShowError(inFilePath, showError);

                else

                {

                    var bw = new BackgroundWorker();

                    bw.DoWork += (s, e) =>

                    {

                        e.Result = false;

                        while (true)

                        {

                            try

                            {

                                FileUtil.WriteLines(outFilePath, process.Process(FileUtil.GetLines(inFilePath)));

                                e.Result = true;

                            }

                            catch (FileNotFoundException)

                            {

                                FileNotFoundShowError(inFilePath, showError);

                            }

                            catch (DirectoryNotFoundException)

                            {

                                DirectoryNotFoundShowError(outFilePath, showError);

                            }

                            catch (IOException)

                            {

                                if (showErrorWithRetry(string.Format(FileErrorResources.IOException, outFilePath)))

                                    continue;

                            }

                            catch (Exception ex)

                            {

                                showError(string.Format(FileErrorResources.UnknowException,

                                    string.Concat(ex.GetType(), ” : “, ex.Message)));

                            }

                            break;

                        }

                        _traitmentsRunning.Remove(outFilePath);

                    };

                    bw.RunWorkerCompleted += (s, e) =>

                    {

                        if (traitmentDone != null)

                            traitmentDone((bool)e.Result);

                    };

                    StartBWAndCache(inFilePath, outFilePath, bw);

                }

            }

        }

    }

 

    private static void StartBWAndCache(string inFilePath, string outFilePath, BackgroundWorker bw)

    {

        lock (_bwLockObject)

        {

            string filePath = null;

            if (_traitmentsRunning.ContainsKey(inFilePath))

                filePath = inFilePath;

            else if (_traitmentsRunning.ContainsKey(outFilePath))

                filePath = outFilePath;

            if (filePath != null)

                _traitmentsRunning[filePath].RunWorkerCompleted +=

                    (s, e) => StartBWAndCache(inFilePath, outFilePath, bw);

            else

            {

                _traitmentsRunning.Add(outFilePath, bw);

                bw.RunWorkerAsync();

            }

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.FileNotFoundException, filePath));

    }

}


With our implementation, it should be the developer, in the Processes.ProcessesAllowed, who will choose the possible processes aggregate.


We will add two new needs in specifications.


  • The user should choose himself the processes aggregation.
  • You have to be able to add some new processes in a new assembly

For this, we will change our architecture.


For this, the user can directly instantiate the processes in the UI or we can use a Factory in BLL.


The first point is very easy to do but it isn’t great because it means that UI layer has to know how the BLL works. Moreover, if we want to add a new process, we would also have to change the UI layer. So we will use the Factory to reduce dependences between our UI layer and our BLL.


To answer the second new need, we will use an Abstract Factory (note that we will change the CompositeStringsProcess class).


public interface IStringsProcessInstanciator

{

    IStringsProcess CreateInstance();

}

 

public class ReverseCharsPerStringProcessInstanciator : IStringsProcessInstanciator

{

    public IStringsProcess CreateInstance()

    {

        return new ReverseCharsPerStringProcess();

    }

 

    public override string ToString()

    {

        return ProcessResources.ReverseCharsPerStringProcess;

    }

}

 

public class NumberPerStringProcessInstanciator : IStringsProcessInstanciator

{

    public IStringsProcess CreateInstance()

    {

        return new NumberPerStringProcess();

    }

 

    public override string ToString()

    {

        return ProcessResources.NumberPerStringProcess;

    }

}

 

public class CompositeStringsProcess : IStringsProcess

{

    private List<IStringsProcessInstanciator> _processes = new List<IStringsProcessInstanciator>();

 

    public CompositeStringsProcess(IEnumerable<IStringsProcessInstanciator> processes)

    {

        _processes.AddRange(processes);

    }

 

    public CompositeStringsProcess(params IStringsProcessInstanciator[] processes)

        : this((IEnumerable<IStringsProcessInstanciator>)processes)

    {

    }

 

    public List<IStringsProcessInstanciator> Processes

    {

        get { return _processes; }

    }

 

    public IEnumerable<string> Process(IEnumerable<string> lines)

    {

        foreach (var process in Processes)

            lines = process.CreateInstance().Process(lines);

        return lines;

    }

 

    public override string ToString()

    {

        var sb = new StringBuilder();

        foreach (var process in Processes)

        {

            sb.Append(” & “);

            sb.Append(process);

        }

        return sb.ToString().Substring(3);

    }

}

 

public static class FileProcess

{

    private static Dictionary<string, BackgroundWorker> _processesRunning =

        new Dictionary<string, BackgroundWorker>();

    private static object _lockObject = new object();

 

    public static void Process(string inFilePath, string outFilePath,

        IEnumerable<IStringsProcessInstanciator> processes, Action<string> showError,

        Func<string, bool> showErrorWithRetry, Action<bool> processDone)

    {

        if (!(File.Exists(inFilePath) || _processesRunning.ContainsKey(inFilePath)))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            var bg = new BackgroundWorker();

            bg.DoWork += (s, e) =>

            {

                e.Result = false;

                while (true)

                {

                    try

                    {

                        FileUtil.WriteLines(outFilePath,

                            new CompositeStringsProcess(processes).Process(FileUtil.GetLines(inFilePath)));

                        e.Result = true;

 

                    }

                    catch (FileNotFoundException)

                    {

                        FileNotFoundShowError(inFilePath, showError);

                    }

                    catch (DirectoryNotFoundException)

                    {

                        DirectoryNotFoundShowError(outFilePath, showError);

                    }

                    catch (IOException)

                    {

                        if (showErrorWithRetry(string.Format(FileErrorResources.IOException, outFilePath)))

                            continue;

                    }

                    catch (Exception ex)

                    {

                        showError(string.Format(FileErrorResources.UnknowException,

                            string.Concat(ex.GetType(), ” : “, ex.Message)));

                    }

                    break;

                }

                lock (_lockObject)

                {

                    _processesRunning.Remove(outFilePath);

                }

            };

            bg.RunWorkerCompleted += (s, e) =>

            {

                if (processDone != null)

                    processDone((bool)e.Result);

            };

            StartBGAndCache(inFilePath, outFilePath, bg);

        }

    }

 

    private static void StartBGAndCache(string inFilePath, string outFilePath, BackgroundWorker bg)

    {

        lock (_lockObject)

        {

            RunWorkerCompletedEventHandler runWorkerCompletedTryAgain = null;

            Action<BackgroundWorker> tryAgain = (bgTryAgain) =>

            {

                StartBGAndCache(inFilePath, outFilePath, bg);

                bgTryAgain.RunWorkerCompleted -= runWorkerCompletedTryAgain;

            };

            if (_processesRunning.ContainsKey(inFilePath))

            {

                var bgTryAgain = _processesRunning[inFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else if (_processesRunning.ContainsKey(outFilePath))

            {

                var bgTryAgain = _processesRunning[outFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else

            {

                _processesRunning.Add(outFilePath, bg);

                bg.RunWorkerAsync();

            }

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.FileNotFoundException, filePath));

    }

}


Our property Processes.ProcessesAllowed will now return some IStringsProcessInstanciator instead of IStringsProcess.


public static class Processes

{

    private static List<IStringsProcessInstanciator> _processes;

    public static List<IStringsProcessInstanciator> ProcessesAllowed

    {

        get

        {

            if (_processes == null)

                _processes = new List<IStringsProcessInstanciator>()

                {

                    new NumberPerStringProcessInstanciator(),

                    new ReverseCharsPerStringProcessInstanciator()

                };

            return _processes;

        }

    }

}


Now, we just need other assemblies (which will add new processes) to create two classes per process: one which implements IStringsProcessInstanciator and the “real” process class which implements IStringsProcess.


However, we already have a problem: the UI has to know the processes to mix between the different assemblies.


It should be better if this list of processes could be made dynamically. Moreover, it allows us to add some processes library without modifying the UI. For this, we can use Reflection to get from a list of assemblies all the classes which implements IStringsProcessInstanciator but there is a new technology which is very useful for what we want to do: MEF.


With MEF, the class Processes is useless.


To use MEF, we have to download it from codeplex first.


Then, we have to reference the dll in the UI layer and BLL.


We also have to decorate the processes with Export attributes and specify to MEF what is the “saw process type” for the MEF consumer (the UI in our case).


[Export(typeof(IStringsProcessInstanciator))]

public class ReverseCharsPerStringProcessInstanciator : IStringsProcessInstanciator

{

    public IStringsProcess CreateInstance()

    {

        return new ReverseCharsPerStringProcess();

    }

 

    public override string ToString()

    {

        return ProcessResources.ReverseCharsPerStringProcess;

    }

}

 

[Export(typeof(IStringsProcessInstanciator))]

public class NumberPerStringProcessInstanciator : IStringsProcessInstanciator

{

    public IStringsProcess CreateInstance()

    {

        return new NumberPerStringProcess();

    }

 

    public override string ToString()

    {

        return ProcessResources.NumberPerStringProcess;

    }

}


Now, we will add a new assembly in which we will add a new process: UpperProcess.


public class UpperProcess : PerStringProcessBase

{

    protected override string Process(string line)

    {

        return line.ToUpper();

    }

 

    public override string ToString()

    {

        return ProcessResources.UpperProcess;

    }

}

 

[Export(typeof(IStringsProcessInstanciator))]

public class UpperProcessInstanciator : IStringsProcessInstanciator

{

    public IStringsProcess CreateInstance()

    {

        return new UpperProcess();

    }

 

    public override string ToString()

    {

        return ProcessResources.UpperProcess;

    }

}


This assembly isn’t referenced by the UI. However, we will be able to use the UpperProcess. First, we will define a directory in which all processes libraries will be copied. For example, we choose the exe directory. Then, we have to copy these libraries in this directory. In the debug mode, we can use the Post-build events in VS:


copy $(TargetFileName) ..\..\..\FileProcessUI\bin\Debug\


Then, we just need to use MEF and let it discover the available processes in the UI:


[Import]

IEnumerable<IStringsProcessInstanciator> _processes;

 

private void FileProcessForm_Load(object sender, EventArgs e)

{

    var processesContainer = new CompositionContainer(new DirectoryCatalog(“.”));

    var batch = new CompositionBatch();

    batch.AddPart(this);

    processesContainer.Compose(batch);

    processesCombo.DataSource = _processes;

}


Instead of the ComboBox, we will use two ListBox to let the user aggregate the available processes himself.


Then, we will change the processDone argument of the FileProcess.Process method from Action<bool> to Action<bool, string>:


public static class FileProcess

{

    private static Dictionary<string, BackgroundWorker> _processesRunning =

        new Dictionary<string, BackgroundWorker>();

    private static object _lockObject = new object();

 

    public static void Process(string inFilePath, string outFilePath,

        IEnumerable<IStringsProcessInstanciator> processes, Action<string> showError,

        Func<string, bool> showErrorWithRetry, Action<bool, string> processDone)

    {

        var process = new CompositeStringsProcess(processes);

        if (!(File.Exists(inFilePath) || _processesRunning.ContainsKey(inFilePath)))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            var bg = new BackgroundWorker();

            bg.DoWork += (s, e) =>

            {

                e.Result = false;

                while (true)

                {

                    try

                    {

                        FileUtil.WriteLines(outFilePath, process.Process(FileUtil.GetLines(inFilePath)));

                        e.Result = true;

                    }

                    catch (FileNotFoundException)

                    {

                        FileNotFoundShowError(inFilePath, showError);

                    }

                    catch (DirectoryNotFoundException)

                    {

                        DirectoryNotFoundShowError(outFilePath, showError);

                    }

                    catch (IOException)

                    {

                        if (showErrorWithRetry(string.Format(FileErrorResources.IOException, outFilePath)))

                            continue;

                    }

                    catch (Exception ex)

                    {

                        showError(string.Format(FileErrorResources.UnknowException,

                            string.Concat(ex.GetType(), ” : “, ex.Message)));

                    }

                    break;

                }

                lock (_lockObject)

                {

                    _processesRunning.Remove(outFilePath);

                }

            };

            bg.RunWorkerCompleted += (s, e) =>

            {

                if (processDone != null)

                    processDone((bool)e.Result, process.ToString());

            };

            StartBGAndCache(inFilePath, outFilePath, bg);

        }

    }

 

    private static void StartBGAndCache(string inFilePath, string outFilePath, BackgroundWorker bg)

    {

        lock (_lockObject)

        {

            RunWorkerCompletedEventHandler runWorkerCompletedTryAgain = null;

            Action<BackgroundWorker> tryAgain = (bgTryAgain) =>

            {

                StartBGAndCache(inFilePath, outFilePath, bg);

                bgTryAgain.RunWorkerCompleted -= runWorkerCompletedTryAgain;

            };

            if (_processesRunning.ContainsKey(inFilePath))

            {

                var bgTryAgain = _processesRunning[inFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else if (_processesRunning.ContainsKey(outFilePath))

            {

                var bgTryAgain = _processesRunning[outFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else

            {

                _processesRunning.Add(outFilePath, bg);

                bg.RunWorkerAsync();

            }

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.FileNotFoundException, filePath));

    }

}


Now, in the UI, the method associated to the run button Click event will be the following:


private void runBtn_Click(object sender, EventArgs e)

{

    FileProcess.Process(inFilePathTB.Text, outFilePathTB.Text, processesLB.Items.Cast<IStringsProcessInstanciator>(),

        message => MessageBox.Show(message, Resources.ErrorTitle, MessageBoxButtons.OK, MessageBoxIcon.Error),

        message => MessageBox.Show(message, Resources.ErrorTitle, MessageBoxButtons.RetryCancel, MessageBoxIcon.Error)

            == DialogResult.Retry,

        (result, processName) =>

        {

            if (result)

                processesDoneLB.Items.Add(

                    string.Concat(processName, ” “, Resources.Done, “. “,

                    inFilePathTB.Text, ” -> “, outFilePathTB.Text));

        });

}


We will add a last need: the ability to cancel the running processes. In order to do this, we will add a Cancel method and a read only Cancelled property on the IStringsProcess interface:


public interface IStringsProcess

{

    IEnumerable<string> Process(IEnumerable<string> lines);

    void Cancel();

    bool Cancelled { get; }

}


To factorize our code, we will define a StringsProcessBase class:


public abstract class StringsProcessBase : IStringsProcess

{

    public abstract IEnumerable<string> Process(IEnumerable<string> lines);

 

    public void Cancel()

    {

        Cancelled = true;

    }

 

    public bool Cancelled { get; private set; }

}

 

public abstract class PerStringProcessBase : StringsProcessBase

{

    public override IEnumerable<string> Process(IEnumerable<string> lines)

    {

        Init();

        foreach (string line in lines)

        {

            if (Cancelled)

                yield break;

            yield return Process(line);

        }

    }

 

    protected abstract string Process(string line);

 

    protected virtual void Init()

    {

    }

}

 

public class CompositeStringsProcess : StringsProcessBase

{

    private List<IStringsProcessInstanciator> _processes = new List<IStringsProcessInstanciator>();

 

    public CompositeStringsProcess(IEnumerable<IStringsProcessInstanciator> processes)

    {

        _processes.AddRange(processes);

    }

    public CompositeStringsProcess(params IStringsProcessInstanciator[] processes)

        : this((IEnumerable<IStringsProcessInstanciator>)processes)

    {

    }

 

    public List<IStringsProcessInstanciator> Processes

    {

        get { return _processes; }

    }

 

    public override IEnumerable<string> Process(IEnumerable<string> lines)

    {

        foreach (var process in Processes)

        {

            if (Cancelled)

                break;

            lines = process.CreateInstance().Process(lines);

        }

        return lines;

    }

 

    public override string ToString()

    {

        var sb = new StringBuilder();

        foreach (var process in Processes)

        {

            sb.Append(” & “);

            sb.Append(process);

        }

        return sb.ToString().Substring(3);

    }

}


In the UI, we will add two ListBox: one for running processes and one for run processes.


We will add a new class in the BLL which will be used for these ListBox binding.


public class FileProcessInfo

{

    internal FileProcessInfo()

    {

    }

 

    public IStringsProcess Process { get; internal set; }

    public string InFilePath { get; internal set; }

    public string OutFilePath { get; internal set; }

 

    public override string ToString()

    {

        return string.Concat(Process, ” (“, InFilePath, ”  –>  “, OutFilePath, “)”);

    }

}


Then , we will modify the FileProcess.Process method to return a FileProcessInfo and integrate the cancellation:


public static class FileProcess

{

    private static Dictionary<string, BackgroundWorker> _processesRunning = new Dictionary<string, BackgroundWorker>();

    private static object _lockObject = new object();

 

    public static FileProcessInfo Process(string inFilePath, string outFilePath,

        IEnumerable<IStringsProcessInstanciator> processes, Action<string> showError,

        Func<string, bool> showErrorWithRetry, Action<bool, FileProcessInfo> processDone)

    {

        var process = new CompositeStringsProcess(processes);

        var result = new FileProcessInfo

        {

            Process = process,

            InFilePath = inFilePath,

            OutFilePath = outFilePath

        };

        if (!(File.Exists(inFilePath) || _processesRunning.ContainsKey(inFilePath)))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            var bg = new BackgroundWorker();

            bg.DoWork += (s, e) =>

            {

                e.Result = false;

                while (true)

                {

                    try

                    {

                        FileUtil.WriteLines(outFilePath, process.Process(FileUtil.GetLines(inFilePath)));

                        e.Result = !process.Cancelled;

                    }

                    catch (FileNotFoundException)

                    {

                        FileNotFoundShowError(inFilePath, showError);

                    }

                    catch (DirectoryNotFoundException)

                    {

                        DirectoryNotFoundShowError(outFilePath, showError);

                    }

                    catch (IOException)

                    {

                        if (showErrorWithRetry(string.Format(FileErrorResources.IOException, outFilePath)))

                            continue;

                    }

                    catch (Exception ex)

                    {

                        showError(string.Format(FileErrorResources.UnknowException,

                            string.Concat(ex.GetType(), ” : “, ex.Message)));

                    }

                    break;

                }

                lock (_lockObject)

                {

                    _processesRunning.Remove(outFilePath);

                }

            };

            bg.RunWorkerCompleted += (s, e) =>

            {

                ProcessDone(processDone, result, (bool)e.Result);

            };

            StartBGAndCache(result, bg, processDone);

        }

        return result;

    }

 

    private static void ProcessDone(Action<bool, FileProcessInfo> processDone,

        FileProcessInfo fileProcessInfo, bool processResult)

    {

        if (processDone != null)

            processDone(processResult, fileProcessInfo);

    }

 

    private static void StartBGAndCache(FileProcessInfo fileProcessInfo, BackgroundWorker bg,

        Action<bool, FileProcessInfo> processDone)

    {

        lock (_lockObject)

        {

            if (fileProcessInfo.Process.Cancelled)

            {

                ProcessDone(processDone, fileProcessInfo, false);

                return;

            }

            var inFilePath = fileProcessInfo.InFilePath;

            var outFilePath = fileProcessInfo.OutFilePath;

            RunWorkerCompletedEventHandler runWorkerCompletedTryAgain = null;

            Action<BackgroundWorker> tryAgain = (bgTryAgain) =>

            {

                StartBGAndCache(fileProcessInfo, bg, processDone);

                bgTryAgain.RunWorkerCompleted -= runWorkerCompletedTryAgain;

            };

            if (_processesRunning.ContainsKey(inFilePath))

            {

                var bgTryAgain = _processesRunning[inFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else if (_processesRunning.ContainsKey(outFilePath))

            {

                var bgTryAgain = _processesRunning[outFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else

            {

                _processesRunning.Add(outFilePath, bg);

                bg.RunWorkerAsync();

            }

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(FileErrorResources.FileNotFoundException, filePath));

    }

}


The fact that the execution of only one process uses a CompositeStringsProcess is a shame (or not, according to your scenario). However, for this post, we will consider that it’s a shame and we will “fix” it.


To manage the case where there is no process in the collection, we will use a StringsNoProcess class:


internal class StringsNoProcess : StringsProcessBase

{

    public override IEnumerable<string> Process(IEnumerable<string> lines)

    {

        return lines;

    }

 

    public override string ToString()

    {

        return “”;

    }

}


Then, we will change (again) the FileProcess.Process method:


Instead of this:


var process = new CompositeStringsProcess(processes);


we will have this:


IStringsProcess process;

if (processes.Skip(1).Any())

    process = new CompositeStringsProcess(processes);

else

{

    var transformInstanciator = processes.FirstOrDefault();

    process = transformInstanciator == null ? new StringsNoProcess() :

    transformInstanciator.CreateInstance();

}


Now in the UI, the runBtn_Click will become the following:


private void runBtn_Click(object sender, EventArgs e)

{

    lock (_lockObject)

    {

        runningProcessesLB.Items.Add(FileProcess.Process(inFilePathTB.Text, outFilePathTB.Text,

            processesLB.Items.Cast<IStringsProcessInstanciator>(),

            message => MessageBox.Show(message, Resources.ErrorTitle, MessageBoxButtons.OK, MessageBoxIcon.Error),

            message =>

                MessageBox.Show(message, Resources.ErrorTitle, MessageBoxButtons.RetryCancel, MessageBoxIcon.Error)

                == DialogResult.Retry,

            (result, fileProcess) =>

            {

                lock (_lockObject)

                {

                    if (result)

                        processesDoneLB.Items.Add(fileProcess);

                    runningProcessesLB.Items.Remove(fileProcess);

                }

            }));

    }

}


There is a last thing to do: make this code generic. Indeed, an important part of our code isn’t specific to string processes so we will use genericity to make our code more reusable.


public interface IProcess<TSource, TDestination>

{

    IEnumerable<TDestination> Process(IEnumerable<TSource> source);

    void Cancel();

    bool Cancelled { get; }

}


 

public abstract class ProcessBase<TSource, TDestination> : IProcess<TSource, TDestination>

{

    public abstract IEnumerable<TDestination> Process(IEnumerable<TSource> lines);

 

    public void Cancel()

    {

        Cancelled = true;

    }

 

    public bool Cancelled { get; private set; }

}


 

public interface IProcessInstanciator<TSource, TDestination>

{

    IProcess<TSource, TDestination> CreateInstance();

}


 

public class EmptyProcess<T> : ProcessBase<T, T>

{

    public override IEnumerable<T> Process(IEnumerable<T> lines)

    {

        return lines;

    }

 

    public override string ToString()

    {

        return “”;

    }

}


 

public class CompositeProcess<T> : ProcessBase<T, T>

{

    private List<IProcess<T, T>> _processes = new List<IProcess<T, T>>();

 

    public CompositeProcess(IEnumerable<IProcessInstanciator<T, T>> processes)

    {

        _processes.AddRange(processes.Select(s => s.CreateInstance()));

    }

    public CompositeProcess(params IProcessInstanciator<T, T>[] processes)

        : this((IEnumerable<IProcessInstanciator<T, T>>)processes)

    {

    }

 

    public List<IProcess<T, T>> Processes

    {

        get { return _processes; }

    }

 

    public override IEnumerable<T> Process(IEnumerable<T> elts)

    {

        foreach (var process in Processes)

        {

            if (Cancelled)

                break;

            elts = process.Process(elts);

        }

        return elts;

    }

 

    public override string ToString()

    {

        var sb = new StringBuilder();

        foreach (var process in Processes)

        {

            sb.Append(” & “);

            sb.Append(process);

        }

        return sb.ToString().Substring(3);

    }

}


Finally, to make our code even more reusable, we will divide the BLL on multiple dll: one specific for file, one specific for string processes and one specific for generic processes.


To conclude, I know that there are many notions seen very quickly but I hope that this post can be helpful in your future architecture.


If you have some critics or different points of view, don’t hesitate to add a comment.


You can download the last version of my code here.

This entry was posted in 10499, 10500, 7672, 7871. Bookmark the permalink.

3 Responses to POO is so pretty with C#

  1. Morshed Anwar says:

    Sir , Can you please re-upload the your sample code. I cant download your code.thanx :).

  2. Matthieu MEZIL says:

    I don’t know what happens. I updated the link. It should be good now.
    Sorry for the delay but I changed of my laptop and I didn’t keep this project.

  3. This thing really helps me through my needs. Great job! It really works!

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>