Search code examples
c#asp.net-mvccontroller

How to use ControllerContext of a controller from another controller in ASP.NET MVC and C#?


I know this has been asked before, and I have read a lot of posts about it, but I can't seem to find anything to help.

I have about 15 controllers that(among many other things) output different documents as ASP.NET MVC views. I am trying to create a method that will combine a variety of these different documents as a PDF using SyncFusion by generating individual PDFs and then merging them (the merge routine is not included in the code below).

Here is a portion of my main method that creates the individual PDF files:

foreach (FormsSubmitted fs in formObjects)
{
    filename = "";

    switch (fs.FormName.ToUpper())
    {
        case "OT EVAL":
            OTEvalController cOTEVAL = new OTEvalController();
            filename = cOTEVAL.CreateOTEvalPrintPDF(fs.VisitId.ToString());
            cOTEVAL = null;
            break;

        case "OT POC":
            OTPOCController cOTPOC = new OTPOCController();
            filename = cOTPOC.CreatePrintOTPlanOfCarePDF(fs.VisitId.ToString());
            cOTPOC = null;
            break;

        case "OT PROGRESS NOTE":
            ProgressNoteController cOTPN = new ProgressNoteController();
            filename = cOTPN.CreateOTProgressNotePrintPDF(fs.VisitId.ToString());
            cOTPN = null;
            break;

        case "PT EVAL":
            PTEvalController cPTEVAL = new PTEvalController();
            filename = cPTEVAL.CreatePTEvalPrintPDF(fs.VisitId.ToString());
            cOTEVAL = null;
            break;

        case "PT POC":
            PTPOCController cPTPOC = new PTPOCController();
            filename = cPTPOC.CreatePTPOCPrintPDF(fs.VisitId.ToString());
            cPTPOC = null;
            break;

        case "PT PROGRESS NOTE":
            PTProgressNoteController cPTPN = new PTProgressNoteController();
            filename = cPTPN.CreatePrintPTProgressNotePDF(fs.VisitId.ToString());
            cPTPN = null;
            break;

        case "SP EVAL":
            SPEvalController cSPEVAL = new SPEvalController();
            filename = cSPEVAL.CreatePrintSPEvalPDF(fs.VisitId.ToString());
            cSPEVAL = null;
            break;

        case "SP POC":
            SPPOCController cSPPOC = new SPPOCController();
            filename = cSPPOC.CreatePrintSPPOCPDF(fs.VisitId.ToString());
            cSPPOC = null;
            break;

        case "SP PROGRESS NOTE":
            SPProgressNoteController cSPPN = new SPProgressNoteController();
            filename = cSPPN.CreatePrintSPProgressNotePDF(fs.VisitId.ToString());
            cSPPN = null;
            break;

        case "MSW EVAL":
            MSWEvalController cMSWEVAL = new MSWEvalController();
            filename = cMSWEVAL.CreateMSWEvalPrintPDF(fs.VisitId.ToString());
            cMSWEVAL = null;
            break;

        case "MSW POC":
            MSWPOCController cMSWPOC = new MSWPOCController();
            filename = cMSWPOC.CreateMSWPOCPrintPDF(fs.VisitId.ToString());
            cMSWPOC = null;
            break;

        case "COMMUNICATION/COORDINATION OF CARE":
            CommController cCOMM = new CommController();
            filename = cCOMM.CreatePrintCommunicationFormPDF(fs.VisitId.ToString());
            cCOMM = null;
            break;
    }

    if (filename != "")
        filenames.Add(filename);
}

This is an example of one of the methods that creates a single PDF:

public string CreatePrintPTProgressNotePDF(string visitid)
{
    string filename = string.Format("PTProgressNote{0}.pdf", visitid);
    string DestPath = Path.Combine(HttpRuntime.AppDomainAppPath, "UploadedDocuments", "InvoiceForms", filename);

    if (!System.IO.File.Exists(DestPath))
    {
        // Getting Index view page as HTML
        ViewEngineResult viewResult = ViewEngines.Engines.FindView(ControllerContext, "PrintPTProgressNotePDF", "");

        PrintPTProgressNoteViewModel ptModel = PrintPTProgressNotePDF(visitid);

        string html = Utility.GetHtmlFromView(ControllerContext, viewResult, "PrintPTProgressNotePDF", ptModel, HttpContext.Request.Url.Scheme, HttpContext.Request.Url.Authority);
        string baseUrl = string.Empty;

        // Convert the HTML string to PDF using WebKit
        Syncfusion.HtmlConverter.HtmlToPdfConverter htmlConverter = new Syncfusion.HtmlConverter.HtmlToPdfConverter(Syncfusion.HtmlConverter.HtmlRenderingEngine.WebKit);

        Syncfusion.HtmlConverter.WebKitConverterSettings settings = new Syncfusion.HtmlConverter.WebKitConverterSettings();

        // Assign WebKit settings to HTML converter
        htmlConverter.ConverterSettings = settings;

        // Convert HTML string to PDF
        Syncfusion.Pdf.PdfDocument document = htmlConverter.Convert(html, baseUrl);

        document.Save(DestPath);
        document.Close(true);
    }

    return DestPath;
}

This is the GetHtmlFromView method that is being used above that also uses the ControllerContext:

public static string GetHtmlFromView(ControllerContext context, ViewEngineResult viewResult, string viewName, object model,
                string Scheme, string Authority)
{
    context.Controller.ViewData.Model = model;

    using (StringWriter sw = new StringWriter())
    {
        // view not found, throw an exception with searched locations
        if (viewResult.View == null)
        {
            var locations = new StringBuilder();
            locations.AppendLine();

            foreach (string location in viewResult.SearchedLocations)
            {
                locations.AppendLine(location);
            }

            throw new InvalidOperationException(
                string.Format(
                    "The view '{0}' or its master was not found, searched locations: {1}", viewName, locations));
        }

        ViewContext viewContext = new ViewContext(context, viewResult.View, context.Controller.ViewData, context.Controller.TempData, sw);
        viewResult.View.Render(viewContext, sw);

        string html = sw.GetStringBuilder().ToString();
        string baseUrl = string.Format("{0}://{1}", Scheme, Authority);
        html = Regex.Replace(html, "<head>", string.Format("<head><base href=\"{0}\" />", baseUrl), RegexOptions.IgnoreCase);
        return html;
    }
}

The Create PDF methods work from their parent controllers, but when I call them from the other controller, I get ControllerContext as null.

Any ideas?


Solution

  • The help page for the ControllerBase.ControllerContext states:

    IControllerActivator activates this property while activating controllers. If user code directly instantiates a controller, the getter returns an empty ControllerContext.

    I tried to if I could resolve IControllerActivator and have that one active the controller, but the IControllerActivator also requires a controller context, so that comes with the same issue...

    I assume the foreach (FormsSubmitted fs in formObjects) part of your code is in the parent controller you mentioned?

    So option 1:

    Just pass in the parent context into the child:

    OTEvalController cOTEVAL = new OTEvalController() { ControllerContext = this.ControllerContext };
    

    Option 2:

    Use the IControllerActivator (or IControllerFactory) as the help suggests, you'd do that like this:

    var controllerFactory = this.HttpContext.RequestServices.GetRequiredService<IControllerFactory>();
    
    var newContext = new ControllerContext(this.ControllerContext)
    {
        ActionDescriptor =
        {
            ControllerName = nameof(OTEvalController),
            ControllerTypeInfo = new TypeDelegator(typeof(OTEvalController)),
            MethodInfo = typeof(OTEvalController).GetMethod("CreateOTEvalPrintPDF")
        }
    };
    
    var resultController = controllerFactory.CreateController(newContext);
    

    Option 3:

    Create the controller with manually with the manually created context:

    var resultController = new OTEvalController(_mediator) { ControllerContext = newContext }; // newContext from Option 2
    

    The downside of this option is though that if your controller takes any constructor parameters, you also have to provide them manually. With option 2 it resolves the controller and all it's dependencies

    Sidenote:

    This kind of a setup seems pretty weird by itself anyways. I'd suggest to refactor your controllers/logic so it's not depended on the ControllerContext. like move the logic from the controller to a separate "manager" class that's not dependent on Controller properties. You controllers should then extract the data that the "manager class", and pass it on to them.

    Try making a unittest in which you can call the entire flow without being depended on Controller stuff