1
\$\begingroup\$

I am trying to generate a Word document with two different tables inside it. For this purpose I have two similar methods where I am passing word document reference and data object and table to the similar methods.

I am looking to make single generic method. So in different places I can use single method and passing parameters to it.

Method 1:

    private static List<OpenXmlElement> RenderExhaustEquipmentTableDataAndNotes(MainDocumentPart mainDocumentPart, List<ProjectObject<ExhaustEquipment>> exhaustEquipment,Table table)
    {
        HtmlConverter noteConverter = new HtmlConverter(mainDocumentPart);
        var equipmentExhaustTypes = new Dictionary<string, List<ProjectObject<ExhaustEquipment>>>();

        foreach (var item in exhaustEquipment)
        {
            string exhaustEquipmentName = item.TargetObject.Name;
            if (!equipmentExhaustTypes.ContainsKey(exhaustEquipmentName))
            {
                equipmentExhaustTypes.Add(exhaustEquipmentName, new List<ProjectObject<ExhaustEquipment>>());
            }
            equipmentExhaustTypes[exhaustEquipmentName].Add(item);
        }

        List<OpenXmlElement> notes = new List<OpenXmlElement>();
        int noteIndex = 1;
        foreach (var exhaustEquipmentItem in equipmentExhaustTypes)
        {
            List<string> noteIndices = new List<string>();
            for (int exhaustEquipmentConditionIndex = 0; exhaustEquipmentConditionIndex < exhaustEquipmentItem.Value.Count; exhaustEquipmentConditionIndex++)
            {
                var condition = exhaustEquipmentItem.Value[exhaustEquipmentConditionIndex];
                var row = new TableRow();
                Run superscriptRun = new Run(new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }));

                if (exhaustEquipmentConditionIndex == 0)
                {              
                    row.Append(RenderOpenXmlElementContentCell(new Paragraph(
                        new List<Run> {
                            new Run(new RunProperties(), new Text(exhaustEquipmentItem.Key) { Space = SpaceProcessingModeValues.Preserve }),
                            superscriptRun
                        }), 1,
                        new OpenXmlElement[] {new VerticalMerge { Val = MergedCellValues.Restart },new TableCellMargin {
                                LeftMargin = new LeftMargin { Width = "120" },
                                TopMargin = new TopMargin { Width = "80" } }
                        }));
                }
                else
                {
                    row.Append(RenderTextContentCell(null, 1, null, null, new OpenXmlElement[] { new VerticalMerge { Val = MergedCellValues.Continue } }));
                }
                row.Append(RenderTextContentCell(condition.TargetObject.IsConstantVolume ? "Yes" : "No"));
                row.Append(RenderTextContentCell($"{condition.TargetObject.MinAirflow:R2}"));
                row.Append(RenderTextContentCell($"{condition.TargetObject.MaxAirflow:R2}"));

                if (condition.TargetObject.NotesHTML?.Count > 0)
                {
                    foreach (var note in condition.TargetObject.NotesHTML)
                    {
                        var compositeElements = noteConverter.Parse(note);
                        var htmlRuns = compositeElements.First().ChildElements.Where(c => c is Run).Cast<Run>().Select(n => n.CloneNode(true));
                        notes.Add(new Run(htmlRuns));
                        noteIndices.Add(noteIndex++.ToString(CultureInfo.InvariantCulture));
                    }
                }                   
                if (exhaustEquipmentConditionIndex == exhaustEquipmentItem.Value.Count - 1 && condition.TargetObject.NotesHTML?.Count > 0)
                {
                    superscriptRun.Append(new Text($"({String.Join(',', noteIndices)})") { Space = SpaceProcessingModeValues.Preserve });
                }
                table.Append(row);
            }
        }
        List<OpenXmlElement> notesSection = new List<OpenXmlElement>();
        List<OpenXmlElement> result = RenderNotesArray(table, notes, notesSection);
        return result;
    }

This is how I am using the above method:

 var table = new Table(RenderTableProperties());
 table.Append(new TableRow(
                    RenderTableHeaderCell("Name"),
                    RenderTableHeaderCell("Constant Volume"),
                    RenderTableHeaderCell("Minimum Airflow", units: "(cfm)"),
                    RenderTableHeaderCell("Wet Bulb Temperature", units: "(cfm)")
                    ));
body.Append(RenderExhaustEquipmentTableDataAndNotes(mainDocumentPart, designHubProject.ExhaustEquipment, table));

Method 2:

    private static List<OpenXmlElement> RenderInfiltrationTableData(MainDocumentPart mainDocumentPart, List<ProjectObject<Infiltration>> infiltration,Table table)
    {
        HtmlConverter noteConverter = new HtmlConverter(mainDocumentPart);
        var nameByInflitrationObject = new Dictionary<string, List<ProjectObject<Infiltration>>>();

        foreach (var infiltrationData in infiltration)
        {
            string infiltrationName = infiltrationData.TargetObject.Name;
            if (!nameByInflitrationObject.ContainsKey(infiltrationName))
            {
                nameByInflitrationObject.Add(infiltrationName, new List<ProjectObject<Infiltration>>());
            }
            nameByInflitrationObject[infiltrationName].Add(infiltrationData);
        }

        List<OpenXmlElement> notes = new List<OpenXmlElement>();
        int noteIndex = 1;

        foreach (var inflitrationDataItem in nameByInflitrationObject)
        {
            List<string> noteIndices = new List<string>();
            for (int inflitrationNameIndex = 0; inflitrationNameIndex < inflitrationDataItem.Value.Count; inflitrationNameIndex++)
            {
                var dataItem = inflitrationDataItem.Value[inflitrationNameIndex];
                var row = new TableRow();
                Run superscriptRun = new Run(new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }));

                if (inflitrationNameIndex == 0)
                {
                    row.Append(RenderOpenXmlElementContentCell(new Paragraph(
                        new List<Run> {
                            new Run(new RunProperties(), new Text(inflitrationDataItem.Key) { Space = SpaceProcessingModeValues.Preserve }),superscriptRun
                        }), 1,
                        new OpenXmlElement[] {new VerticalMerge { Val = MergedCellValues.Restart },new TableCellMargin {
                                LeftMargin = new LeftMargin { Width = "120" },
                                TopMargin = new TopMargin { Width = "80" }}
                        }));
                }
                else
                {
                    row.Append(RenderTextContentCell(null, 1, null, null, new OpenXmlElement[] { new VerticalMerge { Val = MergedCellValues.Continue } }));
                }
                row.Append(RenderTextContentCell($"{dataItem.TargetObject.AirflowScalar.ToString("R2", CultureInfo.CurrentCulture)} cfm {EnumUtils.StringValueOfEnum(dataItem.TargetObject.InfiltrationCalculationType).ToLower(CultureInfo.CurrentCulture)}"));

                if (dataItem.TargetObject.NotesHTML?.Count > 0)
                {
                    foreach (var note in dataItem.TargetObject.NotesHTML)
                    {
                        var compositeElements = noteConverter.Parse(note);
                        var htmlRuns = compositeElements.First().ChildElements.Where(c => c is Run).Cast<Run>().Select(n => n.CloneNode(true));
                        notes.Add(new Run(htmlRuns));
                        noteIndices.Add(noteIndex++.ToString(CultureInfo.InvariantCulture));
                    }
                }

                if (inflitrationNameIndex == inflitrationDataItem.Value.Count - 1 && dataItem.TargetObject.NotesHTML?.Count > 0)
                {
                    superscriptRun.Append(new Text($"({String.Join(',', noteIndices)})") { Space = SpaceProcessingModeValues.Preserve });
                }
                table.Append(row);
            }
        }
        List<OpenXmlElement> notesSection = new List<OpenXmlElement>();
        List<OpenXmlElement> result = RenderNotesArray(table, notes, notesSection);
        return result;
    }

This is how I am using the above method:

   var table = new Table(RenderTableProperties());
   table.Append(new TableRow(
                    RenderTableHeaderCell("Type"),
                    RenderTableHeaderCell("Air Flow")
                    ));
   body.Append(RenderInfiltrationTableData(mainDocumentPart, designHubProject.Infiltration, table));

I know this is a lot of code, but are there any ways to convert these to a single method. I am using .net core.

Any ideas or suggestions on how I can refactor these two methods into single method would be very grateful.

\$\endgroup\$
2
  • \$\begingroup\$ I find a lot of your question's content to be fairly redundant, after the first time you have explicitly stated that you are looking for a way to merge these two methods any more of the same request are no longer needed. Reading the same thing 4 times, 3 in the body of your question and 1 in the title, is just a poor use of peoples time. \$\endgroup\$
    – Peilonrayz
    Commented Jun 6, 2020 at 17:37
  • \$\begingroup\$ Sorry for confusion, I am looking a way to extract common methods or possible make generic method out of it \$\endgroup\$
    – Glory Raj
    Commented Jun 6, 2020 at 17:39

2 Answers 2

4
\$\begingroup\$

Wow, those methods are pretty similar.

Initially, these objects: List<ProjectObject<ExhaustEquipment>> exhaustEquipment List<ProjectObject<Infiltration>> infiltration

are only different in which template parameter refers (ExhaustEquipment,Infiltration), so you could do a generic method:

    //this all, could be written as a Generic method, the Generic Parameter would be
    //Infiltration or ExhaustEquipment
    private static List<OpenXmlElement> GenericRenderElement<Element>(MainDocumentPart mainDocumentPart, List<ProjectObject<Element>> element, Table table)

    HtmlConverter noteConverter = new HtmlConverter(mainDocumentPart);
        var nameByBusinessElement = new Dictionary<string, List<ProjectObject<Element>>>();

        string elementName;
        foreach (var element in businessDictionary)
        {
            elementName = element.TargetObject.Name;
            if (!nameByBusinessElement.ContainsKey(elementName))
                nameByBusinessElement.Add(elementName, new List<ProjectObject<Element>>());

            nameByBusinessElement[elementName].Add(element);
        }


    List<OpenXmlElement> notes = new List<OpenXmlElement>();
        int noteIndex = 1;

    foreach (var element in nameByBusinessElement)
        {
            List<string> noteIndices = new List<string>();
            for (int elementNameIdx = 0; elementNameIdx < element.Value.Count; elementNameIdx++)
            {
                var dataItem = element.Value[elementNameIdx];
                var row = new TableRow();
                Run superscriptRun = new Run(new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }));

                if (elementNameIdx == 0)
                {
                    row.Append(RenderOpenXmlElementContentCell(new Paragraph(
                        new List<Run> {
                            new Run(new RunProperties(), new Text(element.Key) { Space = SpaceProcessingModeValues.Preserve }),superscriptRun
                        }), 1,
                        new OpenXmlElement[] {new VerticalMerge { Val = MergedCellValues.Restart },new TableCellMargin {
                                LeftMargin = new LeftMargin { Width = "120" },
                                TopMargin = new TopMargin { Width = "80" }}
                        }));
                }
                else
                {
                    row.Append(RenderTextContentCell(null, 1, null, null, new OpenXmlElement[] { new VerticalMerge { Val = MergedCellValues.Continue } }));
                }
                row.Append(RenderTextContentCell($"{dataItem.TargetObject.AirflowScalar.ToString("R2", CultureInfo.CurrentCulture)} cfm {EnumUtils.StringValueOfEnum(dataItem.TargetObject.InfiltrationCalculationType).ToLower(CultureInfo.CurrentCulture)}"));

                if (dataItem.TargetObject.NotesHTML?.Count > 0)
                {
                    foreach (var note in dataItem.TargetObject.NotesHTML)
                    {
                        var compositeElements = noteConverter.Parse(note);
                        var htmlRuns = compositeElements.First().ChildElements.Where(c => c is Run).Cast<Run>().Select(n => n.CloneNode(true));
                        notes.Add(new Run(htmlRuns));
                        noteIndices.Add(noteIndex++.ToString(CultureInfo.InvariantCulture));
                    }
                }

                if (elementNameIdx == element.Value.Count - 1 && dataItem.TargetObject.NotesHTML?.Count > 0)
                {
                    superscriptRun.Append(new Text($"({String.Join(',', noteIndices)})") { Space = SpaceProcessingModeValues.Preserve });
                }
                table.Append(row);
            }
        }

        return RenderNotesArray(table, notes, new List<OpenXmlElement>());
    }

and invoke it in the other two concrete methods:

private static List<OpenXmlElement> RenderExhaustEquipmentTableDataAndNotes(MainDocumentPart mainDocumentPart, List<ProjectObject<ExhaustEquipment>> exhaustEquipment,Table table) {
    return GenericRenderElement<ExhaustEquipment>(mainDocumentPart, exhaustEquipment, table);
}

private static List<OpenXmlElement> RenderInfiltrationTableData(MainDocumentPart mainDocumentPart, List<ProjectObject<Infiltration>> infiltration,Table table) {
    return GenericRenderElement<Infiltration>(mainDocumentPart, infiltration, table);
}

Well, I hope it has been helpful.

\$\endgroup\$
8
  • \$\begingroup\$ thanks for the input and other difference is appending data to the table rows at here row.Append(RenderTextContentCell($"{dataItem.TargetObject.AirflowScalar.ToString("R2", CultureInfo.CurrentCulture)} cfm {EnumUtils.StringValueOfEnum(dataItem.TargetObject.InfiltrationCalculationType).ToLower(CultureInfo.CurrentCulture)}")); \$\endgroup\$
    – Glory Raj
    Commented Jun 6, 2020 at 17:42
  • \$\begingroup\$ is there any way we can pass these ones from the parent method \$\endgroup\$
    – Glory Raj
    Commented Jun 6, 2020 at 17:44
  • \$\begingroup\$ You could add a lambda as parameter, said lambda would be of generic type <Element> and has two parameters, TableRow row, Element element, when invoking the GenericRenderElement method you sould send the lambda as parameter, in the case of RenderExhaustEquipmentTableDataAndNotes as the embed procedure you need (using castings if needed), and in the case of RenderInfiltrationTableData as the embed procedure for EnumUtils.StringValueOfEnum(//... \$\endgroup\$ Commented Jun 6, 2020 at 17:53
  • \$\begingroup\$ Thanks for the suggestion If possible could you please paste any pseudo code \$\endgroup\$
    – Glory Raj
    Commented Jun 6, 2020 at 17:55
  • 2
    \$\begingroup\$ @EnigmaState For your own growth you should learn how to do that yourself - teach a man to fish rather than give one. Miguel's answer and comment are good, and have told you, as far as I can tell, all you need. The code is very basic, and you've known how to use lambdas for roughly 10 years now, I'm sure you can do it. \$\endgroup\$
    – Peilonrayz
    Commented Jun 6, 2020 at 18:28
2
\$\begingroup\$

I'm only adding some notes here on addition of Miguel answer, his answer gives you what you asked, but what you really want is to divide this method into several methods, each method would handle one thing and only one thing. here is a list of the methods you need :

  1. Converting List<ProjectObject<T>> to Dictionary.
  2. Creating a Note from ProjectObject<T>.
  3. Creating a Note Indices from ProjectObject<T>.
  4. Creating TableRow from ProjectObject<T>.
  5. Creating Run for ProjectObject<T>.

Then, you can create methods to use these single object return methods to return IEnumerable<T> in which would create multiple objects of each method like creating multiple notes, rows, runs ..etc.

if you do this, it'll be very easy to reuse and maintain. and then, you can overload them to add some other requirements, for instance, for the TableRow, you can add RenderTextContentCell overload.

Another thing is to make use of interface. If you implemented an interface that would be implemented on Infiltration and ExhaustEquipment, you would be able to pass that interface to the method instead of the concrete object name like this

private static List<OpenXmlElement> RenderInfiltrationTableData(
    MainDocumentPart mainDocumentPart, 
    List<ProjectObject<IExhaustInfiltration>> exhaustOrInfiltration,
    Table table) 
    { ... }

with this, you would be able to pass one of those two objects, which would easier to make some conditions in the method to switch some cases based on the type like:

var isInfiltration = exhaustOrInfiltration.GetType() == typeof(Infiltration);

if(isInfiltration)
{
    // do something
}
else
{
    // it's ExhaustEquipment
}

here is a pseudo-code example on how your method would be if you done these suggestions :

private static List<OpenXmlElement> RenderTableData(MainDocumentPart mainDocumentPart, List<ProjectObject<IExhaustInfiltration>> exhaustOrInfiltration,Table table)
{
    var exhaustOrInfiltrationTypes = ToDictionary(exhaustOrInfiltration);

     // to be used on notes
    var isInfiltration = exhaustOrInfiltration.GetType() == typeof(Infiltration);
    foreach(var item in exhaustOrInfiltrationTypes)
    {
        // CreateTableRow would contains the add single row, note, and run methods. 
        var tableRows = CreateTableRow(item.Value, isInfiltration);
        table.Append(tableRow); // assuming there is a method accepts (IEnumerable<TableRow>) to add multiple rows at once. 
    }

    return RenderNotesArray(table, notes, notesSection);
}
\$\endgroup\$
7
  • \$\begingroup\$ Thanks for suggestion, Those two are different objects and there is no interface related with those two and I would like to make single method common among those two \$\endgroup\$
    – Glory Raj
    Commented Jun 7, 2020 at 4:54
  • \$\begingroup\$ @EnigmaState use generics,like what Miguel answer. \$\endgroup\$
    – iSR5
    Commented Jun 7, 2020 at 7:46
  • \$\begingroup\$ even i am favorable with Miguel answer but stuck at passing lambda expressions as parameters to it \$\endgroup\$
    – Glory Raj
    Commented Jun 7, 2020 at 15:51
  • \$\begingroup\$ @EnigmaState the easiest way to understand lambda expressions is to use delegate Func<T> if you create a delegate function that would only pass a string, and return bool Func<string, bool>, then just add the RenderTextContentCell inside it, then just pass this function as parameter. \$\endgroup\$
    – iSR5
    Commented Jun 8, 2020 at 13:42
  • \$\begingroup\$ thanks for suggestion I am just looking for some pseudo code \$\endgroup\$
    – Glory Raj
    Commented Jun 8, 2020 at 13:49

Your Answer

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

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