0
\$\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\$
  • \$\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 Jun 6 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\$ – Enigma State Jun 6 at 17:39
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.

| improve this answer | |
\$\endgroup\$
  • \$\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\$ – Enigma State Jun 6 at 17:42
  • \$\begingroup\$ is there any way we can pass these ones from the parent method \$\endgroup\$ – Enigma State Jun 6 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\$ – Miguel Avila Jun 6 at 17:53
  • \$\begingroup\$ Thanks for the suggestion If possible could you please paste any pseudo code \$\endgroup\$ – Enigma State Jun 6 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 Jun 6 at 18:28
1
\$\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);
}
| improve this answer | |
\$\endgroup\$
  • \$\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\$ – Enigma State Jun 7 at 4:54
  • \$\begingroup\$ @EnigmaState use generics,like what Miguel answer. \$\endgroup\$ – iSR5 Jun 7 at 7:46
  • \$\begingroup\$ even i am favorable with Miguel answer but stuck at passing lambda expressions as parameters to it \$\endgroup\$ – Enigma State Jun 7 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 Jun 8 at 13:42
  • \$\begingroup\$ thanks for suggestion I am just looking for some pseudo code \$\endgroup\$ – Enigma State Jun 8 at 13:49

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.