1

I just want to create a test application to dynamically parse controls. I added new Page().ParseControl. I am getting,

System.ArgumentNullException
Value cannot be null.
Parameter name: virtualPath

at System.Web.VirtualPath.Create(String virtualPath, VirtualPathOptions options) 
at System.Web.UI.TemplateControl.ParseControl(String content) 

Also tried BuildManager.CreateInstanceFromVirtualPath but it throws null exception.

Ben Reich
  • 15,516
  • 2
  • 34
  • 55
user960567
  • 28,664
  • 58
  • 165
  • 292

1 Answers1

3

The exception is actually coming from the internal class System.Web.VirtualPath:

// Default Create method
public static VirtualPath Create(string virtualPath) {
    return Create(virtualPath, VirtualPathOptions.AllowAllPath);
}

...

public static VirtualPath Create(string virtualPath, VirtualPathOptions options) {
    ...

    // If it's empty, check whether we allow it
    if (String.IsNullOrEmpty(virtualPath)) {
        if ((options & VirtualPathOptions.AllowNull) != 0) // <- nope
            return null;

        throw new ArgumentNullException("virtualPath"); // <- source of exception
    }

    ...
}

System.Web.UI.Page inherits ParseControl() from System.Web.UI.TemplateControl. So you are ultimately calling...

public Control ParseControl(string content) {
    return ParseControl(content, true);
}

public Control ParseControl(string content, bool ignoreParserFilter) {
    return TemplateParser.ParseControl(content, VirtualPath.Create(AppRelativeVirtualPath), ignoreParserFilter);
}

For reference (from VirtualPathOptions):

internal enum VirtualPathOptions
{
    AllowNull = 1,
    EnsureTrailingSlash = 2,
    AllowAbsolutePath = 4,
    AllowAppRelativePath = 8,
    AllowRelativePath = 16,
    FailIfMalformed = 32,
    AllowAllPath = AllowRelativePath | AllowAppRelativePath | AllowAbsolutePath,
}

Since VirtualPathOptions.AllowAllPath is passed to VirtualPath.Create()...

return Create(virtualPath, VirtualPathOptions.AllowAllPath);

this...

options & VirtualPathOptions.AllowNull

... evaluates to 0, and an ArgumentNullException will be thrown


Please consider the following example.

Default.aspx:

<%@ Page Title="Home Page" Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebFormsTestBed._Default" %>

<html>
<head>
    <title></title>
</head>
<body>
    <form id="formMain" runat="server">
        <asp:Label ID="lblResults" runat="server"></asp:Label>
    </form>
</body>
</html>

Default.aspx.cs:

using System;
using System.Web;
using System.Web.UI;

namespace WebFormsTestBed {
    public partial class _Default : Page {
        protected void Page_Load(object sender, EventArgs e) {
            Control ctl;
            var page = HttpContext.Current.Handler as Page;

            // First, using `HttpContext.Current.Handler as Page`,
            // - already has `AppRelativeVirtualPath` set to `~\Default.aspx`
            if (page != null) {
                ctl = page.ParseControl(@"<asp:TextBox ID=""txtFromCurrentHandler"" runat=""server"" Text=""Generated from `HttpContext.Current.Handler`""></asp:TextBox>");

                if (ctl != null) lblResults.Text = "Successfully generated control from `HttpContext.Current.Handler`";
            }

            // Next, using `new Page()`, setting `AppRelativeVirtualPath`
            // - set `AppRelativeVirtualPath` to `~\`
            var tmpPage = new Page() {
                AppRelativeVirtualPath = "~\\"
            };

            ctl = tmpPage.ParseControl(@"<asp:TextBox ID=""txtFromNewPageWithAppRelativeVirtualPathSet"" runat=""server"" Text=""Generated from `new Page()` with `AppRelativeVirtualPath` set""></asp:TextBox>", true);

            if (ctl != null)
                lblResults.Text +=
                    string.Format("{0}Successfully generated control from `new Page()` with `AppRelativeVirtualPath` set",
                                  lblResults.Text.Length > 0 ? "<br/>" : "");

            // Last, using `new Page()`, without setting `AppRelativeVirtualPath`
            try {
                ctl = new Page().ParseControl(@"<asp:TextBox ID=""txtFromNewPageWithoutAppRelativeVirtualPathSet"" runat=""server"" Text=""Generated from `new Page()` without `AppRelativeVirtualPath` set""></asp:TextBox>", true);

                if (ctl != null)
                    lblResults.Text +=
                        string.Format("{0}Successfully generated control from `new Page()` without `AppRelativeVirtualPath` set",
                                      lblResults.Text.Length > 0 ? "<br/>" : "");
            } catch (ArgumentNullException) {
                lblResults.Text +=
                    string.Format("{0}Failed to generate control from `new Page()` without `AppRelativeVirtualPath` set",
                                  lblResults.Text.Length > 0 ? "<br/>" : "");
            }
        }
    }
}

You can read about this line...

var page = HttpContext.Current.Handler as Page;

at this here.


Result:

    Successfully generated control from `HttpContext.Current.Handler` 
    Successfully generated control from `new Page()` with `AppRelativeVirtualPath`
    Failed to generate control from `new Page()` without `AppRelativeVirtualPath` set

Example usage from WebForms project

This hack is based on this SO answer, which is based on attaching a non-WebForms test harness to a WebForms application.

Starting with the WebForms project that would have been created for the example above, add a new WinForms project.

For the simplest case, we will just modifying Program.cs:

using System;
using System.IO;
using System.Linq;
using System.Web.Hosting;
using System.Windows.Forms;
using System.Web.UI;

namespace WinFormsTestBed {
    public class AppDomainUnveiler : MarshalByRefObject {
        public AppDomain GetAppDomain() {
            return AppDomain.CurrentDomain;
        }
    }

    internal static class Program {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        private static void Main() {
            var appDomain = ((AppDomainUnveiler)ApplicationHost.CreateApplicationHost(
                    typeof(AppDomainUnveiler), "/", Path.GetFullPath("../../../WebFormsTestBed")))
                .GetAppDomain();

            try {
                appDomain.DoCallBack(StartApp);
            } catch (ArgumentNullException ex) {
                MessageBox.Show(ex.Message);
            } finally {
                AppDomain.Unload(appDomain);
            }
        }

        private static void StartApp() {
            var tmpPage = new Page() {
                AppRelativeVirtualPath = "~/Default.aspx"
            };
            var ctl = tmpPage.ParseControl(@"<asp:TextBox ID=""txtFromNewPageWithAppRelativeVirtualPathSet"" runat=""server"" Text=""Generated from `new Page()` with `AppRelativeVirtualPath` set""></asp:TextBox>");

            ctl = ctl == null ||
                  (ctl = ctl.Controls.OfType<System.Web.UI.WebControls.TextBox>().FirstOrDefault()) == null
                ? null
                : ctl;

            MessageBox.Show(ctl == null ? "Failed to generate asp:TextBox"  : "Generated asp:TextBox with ID = " + ctl.ID);
        }
    }
}

You will need to add a reference to System.Web to the WinForms project and make the WebForms project dependent on the WinForms project (this dependency isn't technically necessary, I'll explain below).

You will end up with the following:

enter image description here enter image description here

Create a post-build event in the WinForms project, which will copy the the WinForms output to the WebForms /bin.

xcopy /y "$(ProjectDir)$(OutDir)*.*" "$(ProjectDir)..\WebFormsTestBed\bin\"

Set the WinForms project as the startup project and run it. If you set everything up correctly you should see:

enter image description here

What this does is creates an AppDomain, which is based on the WebForms project, but in the execution context of the WinForms project, and it provides a method for firing a callback method from the WinForms project within the scope of the newly created AppDomain. This will allow you to handle VirtualPath issues correctly within the WebForms project without worrying about the details of mocking up path variables and such.

When the AppDomain is created, it needs to be able to find all resources in its path, which is why the post-build event was created to copy compiled WinForms files to the WebForms /bin folder. This is why the "dependency" is set up from the WebForms project to the WinForms project in the image above.

In the end I don't know how helpful that will be for you. There might be a way to get this all into a single project, or two projects. I'm not going to spend any more time on this without more detail as to why or how you would be using this.

NOTE: the ctl returned from ParseControl() is now a wrapper, with a Controls collection that actually contains the asp:TextBox - I haven't bothered to figure out why yet


Another Alternative

Instead of keeping a dummy WebForms project around, you could try mocking up the AppDomain entirely, so that setting the AppRelativeVirtualPath on the new Page() does not result in...

System.Web.HttpException The application relative virtual path '~/' cannot be made absolute, because the path to the application is not known.

To go about doing this you'll probably want to start by referring back to the source used by the SO answer that I cited above. The SO answer I cited is actually a workaround for this method, which is why I suggested that first, but it requires a valid WebForms project on the same host as the WinForms project.

Community
  • 1
  • 1
Eric Lease
  • 3,894
  • 1
  • 24
  • 43
  • Tried that before I am getting `System.Web.HttpException The application relative virtual path '~/' cannot be made absolute, because the path to the application is not known.` Note I am using this outside web-from. – user960567 Apr 09 '15 at 06:46
  • @user960567 Please see the addition in regards to using ParseControl outside a WebForms project – Eric Lease Apr 09 '15 at 18:05
  • Thanks will check when I get some time. Thanks anyway – user960567 Apr 10 '15 at 13:37
  • I'm trying to do something similar: Using ParseControl "standalone" without an ASP.NET Page object. I want to parse some ASP.NET-markup und dump it back to an HTML string. The solution works, but events like "OnLoad" are NOT called for user controls. Is there a trick to invoke those with such a Page "dummy"? – Tobias81 Nov 15 '16 at 15:53
  • @Tobias81 The control that is generated from `ParseControl` has no idea how it would be used/interpretted within the ASP.NET WebForm lifecycle. I haven't worked with WebForms for about a year, and don't plan on researching your question (sorry). If I was you, I would start by trying to add that generated control to the `ControlCollection` of a `Page` and allowing it to run its life cycle. If you can do that, then you extract the critical markup from the `Page.Response`. But that seems like it would require the entire ASP.NET engine, and would therefore not be very "standalone". Good luck! – Eric Lease Nov 15 '16 at 16:46
  • Thanks for the suggestions. I settled with rewriting everything so the feature is no longer required. I'm generating HTML code for AJAX-Updates from a WebMethod and to fully work, that mechanism would have needed "OnLoad" etc. to work also. I now do those updates via JS only and without ever re-parsing those "evil" UserControls. – Tobias81 Nov 16 '16 at 16:03