5
\$\begingroup\$

The task of the code that is presented for review is to prepare all the data for the export of the selected contract and create a file on this basis.

This is one of the first experiences with a large (for me) program. So I put the tag begginer. In this regard, please use the terms from my code and avoid general description of some concepts without an example (it is difficult for me to understand it).

Context

There are two documents. Contracts is the main document. ActCompletion is derived from Contracts. It contains some already known data from Contracts and adds its own. Contracts describes a table in the database. ActCompletion takes existing Contracts and runtime receives their individual data from the user. They are not stored anywhere in the database. In both cases, the user selects some Contracts from table. Then clicks the export button on either Contracts or ActCompletion.

Contracts and ActCompletion have a template MS Word with MergeFields. After receiving all the necessary data, the dictionary creation is called. The dictionary contains a key as the MergeField name and a value as the text to be inserted instead of MergeField.

In this way, has two variants:

  • Event from the "export contract" button, loading the selected contract from the database, request a path to save, create a dictionary, create a file.

  • Event of the button "export act", loading the selected contract from the database, request additional data for ActCompletion, request a path to save, create dictionary, create a file.

Elements

  • Exporter is a class that combines the two implementations of the DocumentContract and DocumentActCompletion
  • DocumentContract and DocumentActCompletion are encapsulate own model: ModelContract and ModelActCompletionto hide public fields and methods, leaving only one available method.Inherit interface IDocument to force the Export() method to be implemented.
  • ModelContract and ModelActCompletion extend the base class implementation ExporterModel. The model contains the Export() method, which performs the steps described by me after the word "has two variables".
  • ConverterContract and ConverterActCompletion are encapsulate own implementation of ConverterBase. Both override Convert() method in its own way. Engaged in the creation of a dictionary.
  • ShaperField is auxiliary static class. Contains methods for formatting data into string.
  • WordProvider is сreates a file based on the dictionary, template and output path.

File structure

enter image description here

Example

int id = 22; // some id of selected contract from user
Exporter exporter = new Exporter();
exporter.Contract.Export(id);
// or
// exporter.ActCompletion.Export(id);

UPDATE

I created a database in MS Access (with the extension .accdb). The provider to work with this EntityFrameworkCore.Jet.

Code

Contracts and ActCompletion

public partial class Contracts
{
    public Contracts(int id = 0)
    {
        this.Id = id;
        ListKindWorks = new HashSet<ListKindWorks>();
        ListSubjects = new HashSet<ListSubjects>();
    }

    public int Id { get; set; }
    public string Num { get; set; }
    public DateTime DateConclusion { get; set; }
    public int Worker { get; set; }
    public DateTime DateStartWork { get; set; }
    public DateTime DateEndWork { get; set; }
    public double Salary { get; set; }

    public virtual Workers WorkerNavigation { get; set; }
    public virtual ICollection<ListKindWorks> ListKindWorks { get; set; }
    public virtual ICollection<ListSubjects> ListSubjects { get; set; }
}

public class ActCompletion
{
    public Contracts Contract { get; set; }
    public double Salary { get; set; }
    public Dates Dates { get; private set; }

    public ActCompletion()
    {
        Dates = new Dates();
    }
}

public class Dates
{
    public DateTime DateConclusion { get; set; }
    public DateTime DateStart { get; set; }
    public DateTime DateEnd { get; set; }
}

Exporter

class Exporter
{
    public DocumentContract Contract { get; private set; }
    public DocumentActCompletion ActCompletion { get; private set; }

    public Exporter()
    {
        Contract = new DocumentContract();
        ActCompletion = new DocumentActCompletion();
    }

}

IDocument, DocumentContract and DocumentActCompletion

interface IDocument
{
    void Export(int id);
}

class DocumentContract : IDocument
{
    private readonly ModelContract model;

    public DocumentContract()
    {
        model = new ModelContract();
    }

    public void Export(int id)
    {
        model.Export(id);
    }
}

class DocumentActCompletion : IDocument
{
    private readonly ModelActCompletion model;

    public DocumentActCompletion()
    {
        model = new ModelActCompletion();
    }

    public void Export(int id)
    {
        model.Export(id);
    }
}

ModelContract

class ModelContract : ExporterModel
{
    private readonly ConverterContract converter;

    public ModelContract()
    {
        converter = new ConverterContract();
        SelectTemplate();
    }

    public void Export(int id)
    {
        LoadContract(id);
        if (!RequestDocumentPath(FormatDocumentName()))
        {
            return;
        }

        CreateDictionary();
        CreateDocument();
    }

    private string FormatDocumentName()
    {
        return Properties.Settings.Default.WordTemplate_Contract_DefaultName
            .Replace(
                Properties.Settings.Default.WordTemplate_PatternName,
                ShaperField.ShapeShortName(Contract.WorkerNavigation.FullName));
    }

    private void SelectTemplate()
    {
        PathTemplate = Properties.Settings.Default.WordTemplate_Contract_Path;
    }

    private void CreateDictionary()
    {
        Dict = converter.Convert(Contract);
    }
}

ModelActCompletion

class ModelActCompletion : ExporterModel
{
    private readonly ConverterActCompletion converter;

    private ActCompletion ActCompletion;


    public ModelActCompletion()
    {
        converter = new ConverterActCompletion();
        ActCompletion = new ActCompletion();
        SelectTemplate();
    }

    public void Export(int id)
    {
        LoadContract(id);
        if (!RequestActCompletion()
            || !RequestDocumentPath(FormatDocumentName()))
        {
            return;
        }

        CreateDictionary();
        CreateDocument();
    }

    private bool RequestActCompletion()
    {
        using (ActCompletionForm form = new ActCompletionForm(Contract))
        {
            if (form.ShowDialog() == DialogResult.OK)
            {
                ActCompletion = form.Act;
                return true;
            }
            else
            {
                ActCompletion = new ActCompletion();
                return false;
            }
        }
    }

    private string FormatDocumentName()
    {
        return Properties.Settings.Default.WordTempleate_ActCompletion_DefaultName
            .Replace(
            Properties.Settings.Default.WordTemplate_PatternName,
            ShaperField.ShapeShortName(Contract.WorkerNavigation.FullName));
    }

    private void SelectTemplate()
    {
        PathTemplate = Properties.Settings.Default.WordTemplate_ActCompletion_Path;
    }

    private void CreateDictionary()
    {
        Dict = converter.Convert(ActCompletion);
    }

}

ExporterModel

class ExporterModel
{
    private Logger logger = NLog.LogManager.GetCurrentClassLogger();
    private WordProvider wordProvider;
    private string pathDocument;
    private string pathTemplate;

    public string PathTemplate
    {
        get
        {
            return pathTemplate;
        }
        set
        {
            pathTemplate = Environment.CurrentDirectory + value;
        }
    }
    public Contracts Contract { get; private set; }
    public Dictionary<string, string> Dict { get; set; }

    public ExporterModel()
    {
        Contract = new Contracts();
        wordProvider = new WordProvider();
    }

    public void LoadContract(int id)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        using (ModelContext model = new ModelContext())
        {
            Contract = model.Contracts
                .Where(c => c.Id == id)
                .Include(w => w.WorkerNavigation)
                    .ThenInclude(w => w.IssuedNavigation)
                .Include(w => w.WorkerNavigation)
                    .ThenInclude(w => w.BankNavigation)
                .Include(lkw => lkw.ListKindWorks)
                    .ThenInclude(lkw => ((ListKindWorks)lkw).IdKindWorkNavigation)
                .Include(ls => ls.ListSubjects)
                    .ThenInclude(ls => ((ListSubjects)ls).IdSubjectNavigation)
                .Single();
        }

        stopwatch.Stop();
        logger.Debug("Загрузка договора для экспорта: {0}", stopwatch.Elapsed);
    }

    public bool RequestDocumentPath(string documentName)
    {
        using (SaveFileDialog sfd = new SaveFileDialog())
        {
            sfd.Filter = "Документы Word (*.docx)|*.docx";
            sfd.AddExtension = true;
            sfd.RestoreDirectory = true;
            sfd.FileName = documentName;

            if (sfd.ShowDialog() == DialogResult.OK)
            {
                pathDocument = sfd.FileName;
                return true;
            }
            else
            {
                pathDocument = string.Empty;
                return false;
            }
        }
    }

    public void CreateDocument()
    {
        wordProvider.CreateDocument(Dict, PathTemplate, pathDocument);
    }
}

ConverterContract

class ConverterContract
{
    private readonly Converter converter;

    public ConverterContract()
    {
        converter = new Converter();
    }

    public Dictionary<string, string> Convert(Contracts contract)
    {
        return converter.Convert(contract);
    }

    private class Converter : ConverterBase
    {
        public override Dictionary<string, string> Convert(object obj)
        {
            Contracts c = obj as Contracts;

            InitializeDict(c, c.Salary);
            AddKindWork(c.ListKindWorks);
            AddSubject(c.ListSubjects);
            AddDates(c.DateStartWork, c.DateEndWork);
            AddPassport(c.WorkerNavigation);
            AddBank(c.WorkerNavigation);

            return Dict;
        }

        private void AddKindWork(ICollection<ListKindWorks> list)
        {
            Dict.Add(Fields["KIND_WORK"], ShaperField.ShapeKindWork(list));
        }

        private void AddSubject(ICollection<ListSubjects> list)
        {
            Dict.Add(Fields["SUBJECT"], ShaperField.ShapeSubject(list));
        }

        private void AddDates(DateTime start, DateTime end)
        {
            Dict.Add(Fields["DATE_START_CONTRACT"], ShaperField.ShapeDate(start));
            Dict.Add(Fields["DATE_END_CONTRACT"], ShaperField.ShapeDate(end));
        }

        private void AddPassport(Workers w)
        {
            Dict.Add(Fields["ADDRESS"], ShaperField.ShapeAddress(w.Address));
            Dict.Add(Fields["PASSPORT"], ShaperField.ShapePassport(w));
            Dict.Add(Fields["PASSPORT_NUMBER"], ShaperField.ShapePassportNumber(w.PassportNumber));
        }

        private void AddBank(Workers w)
        {
            Dict.Add(Fields["BANK"], ShaperField.ShapeBank(w));
        }
    }
}

ConverterActCompletion

class ConverterActCompletion
{
    private readonly Converter converter;

    public ConverterActCompletion()
    {
        converter = new Converter();
    }

    public Dictionary<string, string> Convert(ActCompletion act)
    {
        return converter.Convert(act);
    }

    private class Converter : ConverterBase
    {
        public override Dictionary<string, string> Convert(object obj)
        {
            ActCompletion act = obj as ActCompletion;

            InitializeDict(act.Contract, act.Salary);
            AddDates(act.Dates);

            return Dict;
        }

        private void AddDates(Dates dates)
        {
            Dict.Add(Fields["DATE_FILL_ACT_COMPLETION"], ShaperField.ShapeDate(dates.DateConclusion));
            Dict.Add(Fields["DATE_START_ACT_COMPLETION"], ShaperField.ShapeDate(dates.DateStart));
            Dict.Add(Fields["DATE_END_ACT_COMPLETION"], ShaperField.ShapeDate(dates.DateEnd));
        }

    }
}

ConverterBase

using Newtonsoft.Json;

abstract class ConverterBase
{
    public Dictionary<string, string> Fields { get; private set; }
    public Dictionary<string, string> Dict { get; private set; }

    public ConverterBase()
    {
        Dict = new Dictionary<string, string>();
        Fields = JsonConvert.DeserializeObject<Dictionary<string, string>>
            (Properties.Settings.Default.MergeFieldDictionaryAsString);
    }

    public void InitializeDict(Contracts c, double salary)
    {
        Dict.Clear();
        AddGeneralField(c);
        AddSalary(salary);
    }

    public abstract Dictionary<string, string> Convert(object obj);


    private void AddGeneralField(Contracts c)
    {
        Dict.Add(Fields["NUM_CONTRACT"], ShaperField.ShapeNum(c.Num));
        Dict.Add(Fields["DATE_FILL_CONTRACT"], ShaperField.ShapeDate(c.DateConclusion));
        Dict.Add(Fields["FULL_NAME"], ShaperField.ShapeFullName(c.WorkerNavigation.FullName));
        Dict.Add(Fields["SHORT_NAME"], ShaperField.ShapeShortName(c.WorkerNavigation.FullName));
    }

    private void AddSalary(double salary)
    {
        ConvertSalary cs = new ConvertSalary();
        cs.setSalaryWithTax(salary);

        Dict.Add(Fields["SALARY_GROSS"], cs.GetSalaryWithTax());
        Dict.Add(Fields["INCOME_TAX_PROCENT"], Properties.Settings.Default.ConvertSalary_IncomeTaxProcent.ToString());
        Dict.Add(Fields["INCOME_TAX_SUM"], cs.GetIncomeTaxValue());
        Dict.Add(Fields["INSURANCE_TAX_PROCENT"], Properties.Settings.Default.ConvertSalary_InsuranceTaxProcent.ToString());
        Dict.Add(Fields["INSURANCE_TAX_SUM"], cs.GetInsuranceTaxValue());
    }
}

ShaperField

static class ShaperField
{
    public static string ShapeNum(string value)
    {
        return value;
    }

    public static string ShapeDate(DateTime value)
    {
        return value.ToLongDateString();
    }

    public static string ShapeFullName(string value)
    {
        return value;
    }

    // TODO: зачем в KindWork и Subject - ToArray()
    public static string ShapeKindWork(ICollection<ListKindWorks> list)
    {
        return string.Join(", ", list.Select(x => "«" + x.IdKindWorkNavigation.Title + "»").ToArray());
    }

    public static string ShapeSubject(ICollection<ListSubjects> list)
    {
        return string.Join(", ", list.Select(x => "«" + x.IdSubjectNavigation.Title + "»").ToArray());
    }

    public static string ShapeAddress(string value)
    {
        return value;
    }

    public static string ShapePassport(Workers w)
    {
        return string.Format("{0} выдан {1} {2} г.", w.PassportSeries, w.IssuedNavigation.Title, w.DateIssued.ToString("dd.MM.yyyy"));
    }

    public static string ShapePassportNumber(string value)
    {
        return value;
    }

    public static string ShapeBank(Workers w)
    {
        return string.Format("{0}\nв {1}", w.BankAccount, w.BankNavigation.Title);
    }

    public static string ShapeShortName(string fullName)
    {
        string[] name = fullName.Split(' ');
        if (TryFormatShortName(name))
        {
            return FormatShortName(name);
        }
        else
        {
            ShowMessage.Error(string.Format("Во время получения инициалов от полного ФИО произошла ошибка. " +
                "Возможно ФИО не содержит трех слов.\nВ случае продолжения, сокращенное имя получит значение \"{0}\"",
                fullName));

            return fullName;
        }
    }

    private static bool TryFormatShortName(string[] name)
    {
        try
        {
            string shortName = FormatShortName(name);
        }
        catch (Exception ex)
        {
            return false;
        }

        return true;
    }

    private static string FormatShortName(string[] name, string pattern = "{0}.{1}. {2}")
    {
        return string.Format(pattern, name[1][0], name[2][0], name[0]);
    }
}

WordProvider

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;

class WordProvider
{
    private WordprocessingDocument document;
    private Dictionary<string, string> _dict;
    private string _templatePath;
    private string _outputPath;
    private string _tempPath;

    public void CreateDocument(Dictionary<string, string> dict, string templatePath, string outputPath)
    {
        _dict = dict;
        _templatePath = templatePath;
        _outputPath = outputPath;
        _tempPath = GetTempPath();

        Create();
    }

    private string GetTempPath()
    {
        return Path.GetTempPath() + Path.GetFileName(_outputPath);
    }

    private void Create()
    {
        File.Copy(_templatePath, _tempPath, true);

        document = WordprocessingDocument.Open(_tempPath, true);
        document.ChangeDocumentType(WordprocessingDocumentType.Document);

        FillMergeFields();

        document.MainDocumentPart.Document.Save();
        document.Close();

        File.Copy(_tempPath, _outputPath, true);
        File.Delete(_tempPath);
    }

    private void FillMergeFields()
    {
        string fieldName;
        foreach (FieldCode field in document.MainDocumentPart.RootElement.Descendants<FieldCode>())
        {
            fieldName = GetFieldName(field);
            // TODO: пропуски в лог
            if (_dict.ContainsKey(fieldName))
            {
                ReplaceMergeFieldWithText(field, _dict[fieldName]);
            }
        }
    }

    private string GetFieldName(FieldCode field)
    {
        return field.Text.Substring(11, field.Text.IndexOf("\\") - 11).Trim();
    }

    private void ReplaceMergeFieldWithText(FieldCode field, string replacementText)
    {
        // Разбор алгоритма кода
        // https://stackoverflow.com/a/57180496/11563179

        if (field == null || replacementText == string.Empty)
        {
            return;
        }

        Run rFldParent = (Run)field.Parent;
        List<Run> runs = new List<Run>();

        runs.Add(rFldParent.PreviousSibling<Run>()); // begin
        runs.Add(rFldParent.NextSibling<Run>()); // separate
        runs.Add(runs.Last().NextSibling<Run>()); // text
        runs.Add(runs.Last().NextSibling<Run>()); // end

        runs.ForEach(r => r.Remove());
        field.Remove(); // instrText

        rFldParent.Append(new Text(replacementText));
    }
}
\$\endgroup\$
  • \$\begingroup\$ The Contracts is partial but the other part is missing. \$\endgroup\$ – t3chb0t Aug 31 at 16:14
  • \$\begingroup\$ @t3chb0t Contracts generated by the database provider. As well as the rest of the code concerning work with database. \$\endgroup\$ – Viewed Aug 31 at 16:16
  • \$\begingroup\$ I understand yet, you still should include it because your code is using these fields/properties and they are nowhere defined. Could you also add a proper tag for the database technology you use, entity framework or ef-core, or ado.net etc? \$\endgroup\$ – t3chb0t Aug 31 at 16:18
  • \$\begingroup\$ Or do you mean this is the generated class and not your custom one? \$\endgroup\$ – t3chb0t Aug 31 at 16:19
  • \$\begingroup\$ @t3chb0t I have a database in MS Access. I gave it to the provider and got a number of classes describing the tables and the model to work with them. My, custom code in this there is no. All generated. \$\endgroup\$ – Viewed Aug 31 at 16:22
2
\$\begingroup\$

My observations...

Leave generated classes alone You have modified Contracts by adding a custom constructor to it. You'll loose this changes when you regenerate the model. This class is partial not without a purpose. If you want to customize it then you should create another file in the same project and within the same namespace and define your own partial class Contract. This one will survive the model generation process.

As far as the changes are concerned initiailzing Id to 0 is unnecessary because the default value for ints is already 0.

Do not initialize ICollection with HashSet Initializing the other two collections with HashSets is something you should definitely document. I usually wouldn't expect these ICollections to be sets and when they are retrieved from the database the most probably aren't so your model behaves inconsistently. It has different functionality when created with new Contracts because you can only add unique items to it and allows adding non-unique items when retrieved with linq. If these fields are always sets then their type should most probably be ISet but I'm not really sure it is supported by Entity Framework. Anyways, this should be documented somewhere and be consistent.

Initialize either everything or nothing The implementation of ActCompletion is also inconsistent as it initializes only Dates to an instance and leaves Contract as null. I find this is strange and should be documented if it serves any purpose.

Change which classes implement IDocument Both classes DocumentContract and DocumentActCompletion redirect the call to Export to another Export. This looks like the other two classes ModelContract and ModelActCompletion should implement this interface. They already can do what the interfaces specifies. There is no need to create a wrapper that doesn't add anything.

ConverterBase should have a more specific name Seeing the code it's difficult to figure out what that converter is for. I see it does something with dictionaries but I'm not sure what. You should use names that are clear about the purpose of a class. This one looks rather like a factory but it's hard to say. It even uses the json-serializer so maybe it's even some serializer... mhmm, who knows.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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