views:

2185

answers:

4

Question: Is it possible in back end code (not in the code behind but in an actual back end class) to load and render a page or control defined in a .aspx or .ascx without having to use Load(path) and instead just create an instance of the page/control class?

I want to be able to do this (from a back end class NOT a code behind):

MyControl myCtl = new MyApp.Views.Shared.MyControl();
String html = Util.ControlToString(myCtl); //I get an empty string & hidden errors

instead of this

public static string ControlToString(string path)
{
    Page pageHolder = new Page();
    MyControl myCtl = (MyControl)pageHolder.LoadControl(path);
    pageHolder.Controls.Add(myCtl);
    StringWriter output = new StringWriter();
    HttpContext.Current.Server.Execute(pageHolder, output, false);
    return output.ToString();
}

Details: In a Asp.net WebApp I occasionally need to render a user control (.ascx) or page (.aspx) as a HTML string. When a page or control inherits from a code behind, its class shows up in intellisense in my back end code and I can create an instance and set properties without getting compile time or run time errors. However, when I try to render the page or control I always get an empty string and upon inspection the page or control shows suppressed internal rendering errors unless I load the page or control using its physical file path.

I think the key issue has to do with when & how the .aspx / .ascx files are runtime compiled. I don't want to create a pre compiled class library of user controls because that would make the design process awkward and I really like the designer features offered by the .aspx / .ascx pages and so I'd love to find a way to make the pages compile in the solution so that they are usable like any other back end class but can still be created using the designer. I want the best of both worlds (1) to be able to edit pages and controls in the designer and (2) create instances and set their properties using back end classes.

A: 

Generally speaking: no.

As far as I know, ASP.NET inherits from your classes to combine the .aspx/.ascx template with your code. This is why your controls show up empty: the code to combine the template with your code is missing. This is usually done by ASP.NET the first time you access a page or user control (that's precisely why the first hit is a little slow: it's actually generating and compiling the hookup-code).

For precompiled websites ASP.NET generates this code as part of your precompiled website's .dll in advance, which is why such sites load quicker. However, IIRC you'll still need to instantiate the generated classes rather than your original classes.

It's a pretty common request, but so far MS has not provided the tools to do this.

Edit: Although I fail to see why you'd want to render a control to an in-memory string, I might have a solution to the build problems.

If you stick to non-compiled .ascx files (using the web site model rather than the web application model), you can actually develop them separately by placing them physically in subfolder of your main project, and treat them as content files only. Then, you can make a separate project with this subfolder as the root folder. You only need to treat the files in this subfolder as web site files, the main project can still be a web application. (Actually recommended, 'cause you don't want the .csproj files included in the main project.)

However, shared code (that is, shared between the controls project and the main project) should be put in a separate library project, so you can compile each project separately without interdependencies.

Using LoadControl within the main project will compile them on the fly (code behind is possible); if you need to set properties, you must however define interfaces in the shared project, implement them on the appropriate user controls and cast the control created by LoadControl to the appropriate interface.

Ruben
I was afraid that was the answer. I'm thinking of setting up a work flow with compiled controls. Am I right that a Compiled Class Library of controls would allow me to do what I want? Any advice on the best practice for using a library like that in a larger app?Thank you!
Glenn
Glenn
Not sure if it helps, but maybe I've got a solution for the build problems. (Original answer edited.)
Ruben
I'd love to hear it.
Glenn
Like I said, I edited the answer.
Ruben
Oops. I see now. If I make a solution that includes both a separate class library of controls and the compiled web app. How to I set the ClassLibrary to automatically publish the dll and the WebApp to automatically get the updated compiled controls dll? Right now I'm experimenting with this and I have to update the Bin file manually even though they are in the same solution.
Glenn
I'm not sur how to do this, sorry.
Ruben
A: 

I developed a solution that solves my problem in VS 2008:

  1. Create Main Site Solution: Create a MVC 1 Website solution in VS 2008
  2. Create Model Class Library: Add a Class Library for the Model Code
  3. Create View Code: Add an "Empty Website" to hold the .ascx pages, and add a reference the model library
  4. Create Deployment Site: Add a deployment project that compiles the "Empty Website" goto the "properties page" and Check: "Merge All outputs into a single assembly" and "Treat as library component" and be sure to UnCheck: "Allow this precompiled site to be updatable"
  5. Reference Deployment Output: In the main project add a reference to the output of the Deployment site.
  6. ASP. - Compiled Controls: Controls show up under the ASP. namespace and are named in two ways A. if the .ascx / aspx page did not declare a "ClassName" then they are named using their folder and file name with underscores ex. <%@ Control Language="C#" ClassName="Admin_Index" %> B. if they did declare a class name then that is their name

  7. List item

Usage: Example code is below

Here is an example usage

public ActionResult Index()
{
    var ctl = new ASP.Directory_FirmProfile();  //create an instance
    ctl.Setup(new MyDataModel);                 //assign data

    //string test = CompiledControl.Render(ctl); //output to string
    return new HtmlCtl.StrongView(ctl);         //output to response
}    



   public class CompiledControl
    {
        public static string Render(Control c)
        {
            Page pageHolder = new Page();
            pageHolder.Controls.Add(c);
            StringWriter output = new StringWriter();
            HttpContext.Current.Server.Execute(pageHolder, output, false);
            return output.ToString();
        }

        public static void Render(Control c, StringWriter output)
        {
            Page pageHolder = new Page();
            pageHolder.Controls.Add(c);
            HttpContext.Current.Server.Execute(pageHolder, output, false);
        }

        public static void Render(Control c, HttpResponseBase r)
        {
            Page pageHolder = new Page();
            pageHolder.Controls.Add(c);
            HttpContext.Current.Server.Execute(pageHolder, r.Output, false);
        }


    }


    public class StrongView : ActionResult
    {
        private Control ctl;
        public StrongView(Control ctl)
        {
            this.ctl = ctl;
        }

        public string VirtualPath{get;set;}


        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            HtmlCtl.CompiledControl.Render(ctl, context.HttpContext.Response);

        }
    }
Glenn
After using my solution for a few weeks I would recommend against it. The extra compile steps and occasional bugs greatly out weigh the small benefits of this method.
Glenn
A: 

I've come up with a simpler solution along the lines of Ruben's advice. It has worked without problems for about a month:

//Example usage

//reference the control
var emailCTL = new HtmlCtl.ControlOnDisk<MyControlType>(@"~\Views\EmailTemplates\MyControlType.ascx");

//if you have a code behind you will get intellisense allowing you to set these properties
// and re-factoring support works most places except the template file. 
emailCTL.c.title = "Hello World "; //title is a property in the code behind
emailCTL.c.data = data; //data is a property in the code behind

string emailBody = emailCTL.RenderStateless(); 



//Helper Class
    public class ControlOnDisk<ControlType> where ControlType : UserControl
    {
        public ControlType c;
        Page pageHolder = new Page();
        public ControlOnDisk(string path)
        {
            var o = pageHolder.LoadControl(path);
            c = (ControlType)o;
            pageHolder.Controls.Add(c);
        }

        public string RenderStateless()
        {

            StringWriter output = new StringWriter();

            // set up dumby context for use in rendering to email stream
            StringBuilder emailMessage = new StringBuilder();
            TextWriter tw = new StringWriter(emailMessage);
            HttpResponse dumbyResponse = new HttpResponse(tw);
            HttpRequest dumbyRequest = new HttpRequest("", "http://InsertURL.com/", ""); //dummy url requierd for context but not used
            HttpContext dumbyContext = new HttpContext(dumbyRequest, dumbyResponse);
            //HttpContextBase dumbyContextBase = new HttpContextWrapper2(dumbyContext);

            dumbyContext.Server.Execute(pageHolder, output, false);
            return output.ToString();

        }
    }
Glenn
+1  A: 

Here is an approach that may help in situations like this.

The "back-end" code may not know where the user control is located, but the User Control does know where it is.

So, in the User Control, add a static method like this:

public partial class MyControl : UserControl
{
  ...
  public static MyControl LoadControl(CustomDto initialData)
  {
    var myControl = 
        (MyControl) 
        ((Page) HttpContext.Current.Handler)
        .LoadControl("~\\UserControlsSecretPath\\MyControl.ascx");
    myControl._initialData = initialData;
    return myControl;
  }
  ...
  private CustomDto _initialData;
}

(The CustomDto is included to illustrate how initial data can be passed to the User Control. If you don't need to do that, take it out!)

With this, the code that loads the user control does not need to know the path to where the user control is physically located. If that location ever changes, then update this one location. All other code that uses this UserControl is unchanged.

In your back-end code, or anywhere else, you can do something this:

var control = MyControl.LoadControl(customDto);
PlaceHolder1.Controls.Add(control);
Glen Little
I like this approach because it puts the magic file path strings close to where they are used and it makes the initialization requirements explicit in the LoadControl call. I had been doing the loading indirectly using generics and then setting the properties afterwards which makes mistakes easier and puts the magic file path strings further from the actual place they are needed.
Glenn