在 ASP.NET 项目之外使用 System.Web.UI.Page.ParseControl()

Using System.Web.UI.Page.ParseControl() outside of an ASP.NET project

我只想创建一个测试应用程序来动态解析控件。我添加了 new Page().ParseControl。我得到了,

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) 

也试过 BuildManager.CreateInstanceFromVirtualPath 但它抛出 null 异常。

异常实际上来自内部 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.PageSystem.Web.UI.TemplateControl 继承 ParseControl()。所以你最终打电话给...

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

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

供参考(来自VirtualPathOptions):

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

由于 VirtualPathOptions.AllowAllPath 传递给 VirtualPath.Create()...

return Create(virtualPath, VirtualPathOptions.AllowAllPath);

这...

options & VirtualPathOptions.AllowNull

... 计算结果为 0,并且 ArgumentNullException 将被抛出


请考虑以下示例。

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/>" : "");
            }
        }
    }
}

您可以阅读此行...

var page = HttpContext.Current.Handler as Page;

在此here


结果:

    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

来自 WebForms 项目的示例用法

此 hack 基于 this SO answer,它基于将非 WebForms 测试工具附加到 WebForms 应用程序。

从为上述示例创建的 WebForms 项目开始,添加一个新的 WinForms 项目。

对于最简单的情况,我们只需修改 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);
        }
    }
}

您需要将 System.Web 的引用添加到 WinForms 项目并使 WebForms 项目依赖于 WinForms 项目(这种依赖在技术上不是必需的,我将在下面解释)。

您将得到以下结果:

在 WinForms 项目中创建一个 post-build 事件,它将 WinForms 输出复制到 WebForms /bin。

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

将 WinForms 项目设置为启动项目并运行它。如果一切设置正确,您应该会看到:

它所做的是创建一个 AppDomain,它基于 WebForms 项目,但在 WinForms 项目的执行上下文中,它提供了一种从 WinForms 项目中触发回调方法的方法新创建的范围AppDomain。这将使您能够在 WebForms 项目中正确处理 VirtualPath 问题,而不必担心模拟路径变量等的细节。

创建AppDomain时,它需要能够找到其路径中的所有资源,这就是创建post-build事件以将已编译的WinForms文件复制到WebForms的原因/bin 文件夹。这也是上图中"dependency"从WebForms项目设置到WinForms项目的原因。

最后不知道对你有多大帮助。可能有一种方法可以将所有这些都整合到一个或两个项目中。如果没有更多关于为什么或如何使用它的详细信息,我不会再花时间在这上面了。

注意:从 ParseControl() 返回的 ctl 现在是一个包装器,Controls 集合实际包含 asp:TextBox - 我还没有费心弄清楚为什么


另一种选择

您可以尝试完全模拟 AppDomain,而不是保留虚拟 WebForms 项目,这样在 new Page() 上设置 AppRelativeVirtualPath 不会导致...

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

要开始执行此操作,您可能需要先参考我上面引用的 SO 答案使用的 source。我引用的 SO 答案实际上是此方法的解决方法,这就是我首先建议的原因,但它需要与 WinForms 项目位于同一主机上的有效 WebForms 项目。