4
\$\begingroup\$

This is the latest edition of my PDF tool, previously mentioned here, here, here, and inspired by my first attempt at a MVP here.

Major differences from previous editions:

  • It uses the MVP pattern (as opposed to the god-tier UI that did everything).
  • The background/separate thread is gone. I will consider adding it back later, but it was giving me pains with the new pattern.
  • I "fixed" every last using directive.
  • I removed every last documentation comment.
  • The processor is now a static class.

"Problems":

  • The UI/View is still not entirely naive. It knows what scripts and fields are, and assigns them depending on which button the user presses. I don't know if there is a better alternative or not.
  • The presenter seems to do a little too much direct modification to the UI. Again, not sure what can/should be done about that.
  • The background thread is gone. This means that very large workloads have the potential to crash the application, due to the Winforms timeout. This can always be avoided by not running several thousand files at a time, but I feel I should do something to "fix" this.
  • The "model" part of the MVP is largely nonexistent. There is simply no data that needs to be handled save a list of strings. The back end only handles actions, which largely do not affect the view.

PdfConversionAndTimeStampTool.cs

namespace PdfConversionAndTimeStampTool
{
    using System.IO;
    using Application = System.Windows.Forms.Application;

    internal static class PdfConversionAndTimeStampTool
    {
        [System.STAThread]
        private static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var view = new PdfConversionAndTimeStampToolView();
            var presenter = new PdfConversionAndTimeStampToolPresenter(view);
            Directory.CreateDirectory(PdfProcessor.OutputPath);
            Directory.CreateDirectory(PdfProcessor.ProcessingPath);
            PdfProcessor.ClearProcessing();
            Application.Run(view);
        }
    }
}

IPdfConversionAndTimeStampView.cs

using System;
using System.Collections.Generic;

namespace PdfConversionAndTimeStampTool
{
    internal interface IPdfConversionAndTimeStampToolView :
        IProgress<ProgressReport>
    {
        event Action FilesSelected;

        event Action TaskRequested;

        Field Field { get; }
        List<string> FileNames { get; set; }
        List<string> OpenFileNames { get; }
        Script Script { get; }

        void ClearFiles();

        void ClearProgress();

        void ShowMessage(string message);

        void ToggleEnabled();
    }
}

PdfConversionAndTimeStampToolView.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using static PdfConversionAndTimeStampTool.Properties.Resources;

namespace PdfConversionAndTimeStampTool
{
    internal partial class PdfConversionAndTimeStampToolView : Form,
        IPdfConversionAndTimeStampToolView
    {
        internal PdfConversionAndTimeStampToolView()
        {
            InitializeComponent();
            BindComponent();
            openFileDialog.Multiselect = true;
            openFileDialog.Filter = OpenFileDialogFilter;
            openFileDialog.Title = OpenFileDialogTitle;
        }

        public event Action FilesSelected;

        public event Action TaskRequested;

        public List<string> CheckedFileNames
        {
            get
            {
                return fileView.CheckedItems.OfType<string>().ToList();
            }
        }

        public Field Field { get; set; }

        public List<string> FileNames
        {
            get
            {
                return openFileDialog.FileNames.ToList();
            }

            set
            {
                foreach (string fileName in value)
                {
                    fileView.Items.Add(fileName, isChecked: true);
                }
            }
        }

        public List<string> OpenFileNames
        {
            get
            {
                return fileView.Items.OfType<string>().ToList();
            }
        }

        public Script Script { get; set; }

        public void ClearFiles()
        {
            fileView.Items.Clear();
        }

        public void ClearProgress()
        {
            progressBar.Value = 0;
        }

        public void Report(ProgressReport progressReport)
        {
            progressBar.Value = progressReport.Percent;
        }

        public void ShowMessage(string message)
        {
            MessageBox.Show(message);
        }

        public void ToggleEnabled()
        {
            Enabled = !Enabled;
        }

        private void BindComponent()
        {
            selectFilesButton.Click += OnSelectFilesButtonClick;
            convertOnlyButton.Click += OnTaskButtonClick;
            convertAndTimeStampDefaultDayButton.Click += OnTaskButtonClick;
            convertAndTimeStampDefaultMonthButton.Click += OnTaskButtonClick;
        }

        private void OnSelectFilesButtonClick(object sender, EventArgs e)
        {
            var DialogResult = openFileDialog.ShowDialog();
            if (DialogResult == DialogResult.OK)
            {
                FilesSelected?.Invoke();
            }
        }

        private void OnTaskButtonClick(object sender, EventArgs e)
        {
            if (CheckedFileNames.Any())
            {
                if (sender == convertOnlyButton)
                {
                    Field = null;
                    Script = null;
                }
                else if (sender == convertAndTimeStampDefaultDayButton)
                {
                    Field = Field.DefaultTimeStampField;
                    Script = Script.TimeStampOnPrintDefaultDayScript;
                }
                else if (sender == convertAndTimeStampDefaultMonthButton)
                {
                    Field = Field.DefaultTimeStampField;
                    Script = Script.TimeStampOnPrintDefaultMonthScript;
                }
                else if (sender == addCustomFieldButton)
                {
                    // TODO
                    // Field = new Field();
                }
                else if (sender == addCustomScriptButton)
                {
                    // TODO
                    // Script = new Script();
                }
                TaskRequested?.Invoke();
            }
            else
            {
                ShowMessage("Please select at least one file for processing.");
            }
        }
    }
}

PdfConversionAndTimeStampToolPresenter.cs

using System.Collections.Generic;
using System.IO;

namespace PdfConversionAndTimeStampTool
{
    internal class PdfConversionAndTimeStampToolPresenter
    {
        private readonly IPdfConversionAndTimeStampToolView view;

        internal PdfConversionAndTimeStampToolPresenter(
            IPdfConversionAndTimeStampToolView view)
        {
            this.view = view;
            this.view.FilesSelected += OnFilesSelected;
            this.view.TaskRequested += OnTaskRequested;
        }

        private void OnFilesSelected()
        {
            var fileNames = new List<string>();
            if (view.FileNames != null)
            {
                foreach (var fileName in view.FileNames)
                {
                    if (fileName.FileNameIsContainedIn(view.OpenFileNames))
                    {
                        view.ShowMessage("File \"" +
                            Path.GetFileNameWithoutExtension(fileName) +
                            "\" is already open.");
                    }
                    else
                    {
                        fileNames.Add(FileProcessor.PrepareFile(fileName));
                    }
                }
                view.FileNames = fileNames;
            }
        }

        private void OnTaskRequested()
        {
            view.ToggleEnabled();
            FileProcessor.ProcessFiles(
                view.OpenFileNames,
                view,
                view.Field,
                view.Script);
            view.ClearFiles();
            view.ToggleEnabled();
            view.ClearProgress();
            view.ShowMessage("Files saved to " + FileProcessor.OutputPath);
        }
    }
}

FileProcessor.cs

using iTextSharp.text.pdf;
using Microsoft.Office.Core;
using Microsoft.Office.Interop.Word;
using System;
using System.Collections.Generic;
using System.IO;
using static PdfConversionAndTimeStampTool.Properties.Resources;
using static System.Environment;

namespace PdfConversionAndTimeStampTool
{
    internal static class FileProcessor
    {
        private const int EveryOtherPage = 2;

        private const int EveryPage = 1;

        private const int FirstPageNumber = 1;

        private const int SecondPageNumber = 2;

        internal static string OutputPath
        {
            get
            {
                return Path.Combine(
                    GetFolderPath(SpecialFolder.MyDocuments),
                    RootFolderName);
            }
        }

        internal static string ProcessingPath
        {
            get
            {
                return Path.Combine(
                    GetFolderPath(SpecialFolder.ApplicationData),
                    RootFolderName);
            }
        }

        internal static void ClearProcessing()
        {
            var processingDirectory = new DirectoryInfo(ProcessingPath);
            foreach (FileInfo file in processingDirectory.GetFiles())
            {
                file.Delete();
            }
        }

        internal static string CopyFileToProcessing(string filename)
        {
            var processingPath = GetProcessingPath(filename);
            File.Copy(filename, processingPath);
            return processingPath;
        }

        internal static string PrepareFile(string fileName)
        {
            return CopyFileToProcessing(fileName);
        }

        internal static List<string> PrepareFiles(List<string> fileNames)
        {
            for (int i = 0; i < fileNames.Count; i++)
            {
                fileNames[i] = CopyFileToProcessing(fileNames[i]);
            }
            return fileNames;
        }

        internal static void ProcessFiles(
            List<string> fileNames,
            IProgress<ProgressReport> progressReport,
            Field field = null,
            Script script = null)
        {
            for (int i = 0; i < fileNames.Count; i++)
            {
                var currentFile = fileNames[i];
                if (!IsPdf(currentFile))
                {
                    currentFile = ConvertToPdf(currentFile);
                }

                if (field != null || script != null)
                {
                    ProcessPdf(currentFile, field, script);
                }
                else
                {
                    MovePdfToOutput(currentFile);
                }
                progressReport.Report(new ProgressReport
                {
                    Total = fileNames.Count,
                    CurrentCount = i + 1
                });
            }
            ClearProcessing();
        }

        private static void AddFieldToPage(
            Field field,
            int pageNumber,
            PdfStamper pdfStamper,
            PdfFormField parentField)
        {
            var textField = new TextField(
                pdfStamper.Writer,
                new iTextSharp.text.Rectangle(
                    field.TopLeftX,
                    field.TopLeftY,
                    field.BottomRightX,
                    field.BottomRightY),
                null);
            var childField = textField.GetTextField();
            parentField.AddKid(childField);
            childField.PlaceInPage = pageNumber;
        }

        private static void AddFieldToPdf(
            Field field, PdfStamper pdfStamper, int numberOfPages)
        {
            var parentField = PdfFormField.CreateTextField(
                pdfStamper.Writer, multiline:false, password:false, maxLen:0);
            parentField.FieldName = field.Title;
            int pageNumber = field.Pages == Pages.Last ?
                numberOfPages : FirstPageNumber;
            if (field.Pages == Pages.First || field.Pages == Pages.Last)
            {
                AddFieldToPage(
                    field,
                    pageNumber,
                    pdfStamper,
                    parentField);
            }
            else
            {
                int increment = field.Pages == Pages.All ?
                    EveryPage : EveryOtherPage;
                if (field.Pages == Pages.Even)
                {
                    pageNumber += 1;
                }

                for (; pageNumber <= numberOfPages; pageNumber += increment)
                {
                    AddFieldToPage(
                        field,
                        pageNumber,
                        pdfStamper,
                        parentField);
                }
            }

            pdfStamper.AddAnnotation(parentField, FirstPageNumber);
        }

        private static void AddScriptToPdf(
            Script script, PdfStamper pdfStamper)
        {
            var pdfAction = PdfAction.JavaScript(
                script.ScriptText, pdfStamper.Writer);
            PdfName actionType = null;
            switch (script.ScriptEvent)
            {
                case ScriptEvent.DidPrint:
                    actionType = PdfWriter.DID_PRINT;
                    break;

                case ScriptEvent.DidSave:
                    actionType = PdfWriter.DID_SAVE;
                    break;

                case ScriptEvent.WillPrint:
                    actionType = PdfWriter.WILL_PRINT;
                    break;

                case ScriptEvent.WillSave:
                    actionType = PdfWriter.WILL_SAVE;
                    break;
            }

            pdfStamper.Writer.SetAdditionalAction(
                actionType, pdfAction);
        }

        private static string ConvertToPdf(string filename)
        {
            var outputFilename = Path.GetFileNameWithoutExtension(filename)
                + PdfFileExtension;
            var outputPath = Path.Combine(ProcessingPath, outputFilename);
            var wordApplication = new Application();
            wordApplication.Application.AutomationSecurity =
                MsoAutomationSecurity.msoAutomationSecurityForceDisable;
            var wordDocument = wordApplication.Documents.Open(filename);
            var exportFormat = WdExportFormat.wdExportFormatPDF;
            wordDocument.ExportAsFixedFormat(outputPath, exportFormat);
            wordDocument.Close(false);
            wordApplication.Quit();
            return outputPath;
        }

        private static string GetOutputPath(string inputPath)
        {
            return Path.Combine(OutputPath, Path.GetFileName(inputPath));
        }

        private static string GetProcessingPath(string inputPath)
        {
            return Path.Combine(ProcessingPath, Path.GetFileName(inputPath));
        }

        private static bool IsPdf(string filename)
        {
            return string.Equals(
                Path.GetExtension(filename),
                PdfFileExtension,
                StringComparison.InvariantCultureIgnoreCase);
        }

        private static string MovePdfToOutput(string filename)
        {
            var outputPath = GetOutputPath(filename);
            File.Move(filename, outputPath);
            return outputPath;
        }

        private static void ProcessPdf(
            string filename, Field field, Script script)
        {
            using (var pdfReader = new PdfReader(filename))
            {
                using (var pdfStamper = new PdfStamper(
                    pdfReader,
                    new FileStream(GetOutputPath(filename), FileMode.Create)))
                {
                    if (field != null)
                    {
                        AddFieldToPdf(
                            field,
                            pdfStamper,
                            pdfReader.NumberOfPages);
                    }

                    if (script != null)
                    {
                        AddScriptToPdf(script, pdfStamper);
                    }
                }
            }
        }
    }
}
\$\endgroup\$

1 Answer 1

4
+50
\$\begingroup\$

You have a bug in this "setter".

    public List<string> FileNames
    {
        get
        {
            return openFileDialog.FileNames.ToList();
        }

        set
        {
            foreach (string fileName in value)
            {
                fileView.Items.Add(fileName, isChecked: true);
            }
        }
    }

You're not actually setting the list. You're appending to it. Setting it twice will result in simply adding all of the new values to the end.


PrepareFile does nothing useful. It just delegates off to another static method that does exactly the same thing.

    internal static string CopyFileToProcessing(string filename)
    {
        var processingPath = GetProcessingPath(filename);
        File.Copy(filename, processingPath);
        return processingPath;
    }

    internal static string PrepareFile(string fileName)
    {
        return CopyFileToProcessing(fileName);
    }

    internal static List<string> PrepareFiles(List<string> fileNames)

This FileProcessor class is also static, I recommend against it. Later, you're going to want to mock it out so you don't hit an actual file system during testing. In order to do that, you'll want to declare an interface and interfaces can't have static methods.


I really dislike this method.

    internal static List<string> PrepareFiles(List<string> fileNames)
    {
        for (int i = 0; i < fileNames.Count; i++)
        {
            fileNames[i] = CopyFileToProcessing(fileNames[i]);
        }
        return fileNames;
    }

First and foremost, you're modifying the List that you passed in. That's a terribly surprising side effect. You could be creating yourself a nasty bug due to unexpected global state. Minimally, it will be harder to maintain due to that global state. Much better to build a new list. And if you're going to build a new list, you might as well use a foreach.

    internal static List<string> PrepareFiles(List<string> fileNames)
    {
        List<string> result;
        foreach (var fileName in fileNames)
        {
            result.Add( CopyFileToProcessing(fileName));
        }
        return result;
    }

Which could easily be rewritten as some Linq.

    internal static List<string> PrepareFiles(List<string> fileNames)
    {
        return fileNames.Select(CopyFileToProcessing).ToList();
    }

Although, I would probably return an IEnumerable<string> instead and omit the ToList() call so that it could be evaluated lazily.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.