我该如何在ASP.NET中获得更多的控制权?

124

我正在尝试构建一个非常简单的“微型Web应用程序”,如果我完成它,我觉得会对一些Stack Overflow用户感兴趣。我将其托管在我的C# in Depth网站上,该网站是纯粹的ASP.NET 3.5(即非MVC)。

流程非常简单:

  • 如果用户输入的URL没有指定所有参数(或任何一个参数无效),我希望只显示用户输入控件(只有两个)。
  • 如果用户输入的URL有所有必需的参数,我希望显示结果和输入控件(以便他们可以更改参数)。

以下是我的自我要求(设计和实现的混合):

  • 我希望提交使用GET而不是POST,这样用户可以轻松地将页面加入书签。
  • 我不想让URL在提交后看起来很傻,而是只保留主URL和真正的参数。
  • 理想情况下,我希望完全避免需要JavaScript。在这个应用中没有好的理由需要它。
  • 我希望能够在渲染时访问控件并设置值等。特别是,如果ASP.NET无法自动为我完成此操作(在其他限制范围内),我希望能够将控件的默认值设置为传递的参数值。
  • 我很乐意自己进行所有参数验证,并且我不需要太多的服务器端事件。在页面加载时设置所有内容非常简单,而不是将事件附加到按钮等。

大多数情况下,这是可以接受的,但我没有找到任何方法完全删除视图状态并保留其余有用功能。使用这篇博客文章中的帖子,我设法避免获取视图状态的实际值,但它仍然最终成为URL上的参数,看起来非常丑陋。

如果我将其更改为普通的HTML表单而不是ASP.NET表单(即去掉runat="server"),那么我就不会得到任何神奇的视图状态,但是我也无法以编程方式访问控件。

我可以通过忽略大部分ASP.NET并使用LINQ to XML构建XML文档,并实现IHttpHandler来完成所有这些工作。虽然感觉有点低级。

我意识到我的问题可以通过放宽约束(例如使用POST并不关心多余的参数)或使用ASP.NET MVC来解决,但我的要求真的不合理吗?

也许ASP.NET只适用于更大型的应用程序?还有一个很可能的替代方案:我可能只是愚蠢,还有一种完全简单的方法可以做到这一点,我只是没有找到。

任何想法,任何人?(预计会有评论称大佬已经跌落神坛之类的话。没关系-我希望我从来没有声称自己是ASP.NET专家,因为事实恰恰相反...)


16
“Cue comments of how the mighty are fallen” - 我们都是无知的,只是无知的事情不同。虽然我最近才开始参与这里,但我比所有积分更欣赏这个问题。你显然还在思考和学习。向你致敬! - duffymo
15
我不认为我会关注放弃学习的人 :) - Jon Skeet
3
你的下一本书会是《深入理解ASP.NET》吗?:-P - chakrit
20
是的,它预计在2025年发布。;) - Jon Skeet
1
@GK:请不要在这里使用我的名字,这会删除一个重要的上下文链接。我从来没有做过这样的事情。 - GEOCHET
显示剩余2条评论
7个回答

76

这个解决方案将为您提供编程访问控件的全部内容,包括控件上的所有属性。此外,在提交时,只有文本框值会出现在URL中,因此您的GET请求URL将更具"意义"

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="JonSkeetForm.aspx.cs" Inherits="JonSkeetForm" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Jon Skeet's Form Page</title>
</head>
<body>
    <form action="JonSkeetForm.aspx" method="get">
    <div>
        <input type="text" ID="text1" runat="server" />
        <input type="text" ID="text2" runat="server" />
        <button type="submit">Submit</button>
        <asp:Repeater ID="Repeater1" runat="server">
            <ItemTemplate>
                <div>Some text</div>
            </ItemTemplate>
        </asp:Repeater>
    </div>
    </form>
</body>
</html>

然后在您的代码后台,您可以在PageLoad上执行所有需要的操作。

public partial class JonSkeetForm : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        text1.Value = Request.QueryString[text1.ClientID];
        text2.Value = Request.QueryString[text2.ClientID];
    }
}

如果你不想要一个有 runat="server" 属性的表单,那么你应该使用HTML控件。它更易于你的目的。只需使用常规的HTML标签并添加 runat="server" 属性以及ID即可。然后你可以通过编程方式访问它们而且不需要ViewState。

唯一的缺点是你将无法访问许多“有用”的ASP.NET服务器控件,如 GridView。我在我的示例中包含了一个 Repeater ,因为我假设你希望在同一页上显示字段和结果,并且(据我所知)只有 Repeater 是能够在没有Form标签中的 runat="server" 属性的情况下运行的数据绑定控件。


1
我只有很少的字段需要手动处理,所以这真的很容易 :) 关键是我不知道我可以在普通的HTML控件中使用runat=server。我还没有实现结果,但那是容易的部分。快要完成了! - Jon Skeet
实际上,即使您在页面级别上设置了EnableViewState ="False",<form runat="server">仍会添加__ VIEWSTATE(和其他一些)隐藏字段。如果您想在页面上丢失ViewState,则应采用这种方式。至于URL友好性,urlrewriting可能是一个选择。 - Sergiu Damian
1
这个答案很好(尽管它意味着拥有一个ID为“user”的控件 - 出于某种原因,我无法单独更改文本框控件的名称而不更改其ID)。 - Jon Skeet
1
只是确认一下,这确实非常好用。非常感谢! - Jon Skeet
14
看起来你应该只是用经典 ASP 来编写它! - ScottE
显示剩余2条评论

12

我认为你做得很好,不使用在FORM标记中的runat="server"。这意味着你需要直接从Request.QueryString中提取值,就像这个例子:

在.aspx页面本身中:

<%@ Page Language="C#" AutoEventWireup="true" 
     CodeFile="FormPage.aspx.cs" Inherits="FormPage" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>ASP.NET with GET requests and no viewstate</title>
</head>
<body>
    <asp:Panel ID="ResultsPanel" runat="server">
      <h1>Results:</h1>
      <asp:Literal ID="ResultLiteral" runat="server" />
      <hr />
    </asp:Panel>
    <h1>Parameters</h1>
    <form action="FormPage.aspx" method="get">
    <label for="parameter1TextBox">
      Parameter 1:</label>
    <input type="text" name="param1" id="param1TextBox" value='<asp:Literal id="Param1ValueLiteral" runat="server" />'/>
    <label for="parameter1TextBox">
      Parameter 2:</label>
    <input type="text" name="param2" id="param2TextBox"  value='<asp:Literal id="Param2ValueLiteral" runat="server" />'/>
    <input type="submit" name="verb" value="Submit" />
    </form>
</body>
</html>

在代码后台:

using System;

public partial class FormPage : System.Web.UI.Page {

        private string param1;
        private string param2;

        protected void Page_Load(object sender, EventArgs e) {

            param1 = Request.QueryString["param1"];
            param2 = Request.QueryString["param2"];

            string result = GetResult(param1, param2);
            ResultsPanel.Visible = (!String.IsNullOrEmpty(result));

            Param1ValueLiteral.Text = Server.HtmlEncode(param1);
            Param2ValueLiteral.Text = Server.HtmlEncode(param2);
            ResultLiteral.Text = Server.HtmlEncode(result);
        }

        // Do something with parameters and return some result.
        private string GetResult(string param1, string param2) {
            if (String.IsNullOrEmpty(param1) && String.IsNullOrEmpty(param2)) return(String.Empty);
            return (String.Format("You supplied {0} and {1}", param1, param2));
        }
    }

这里的技巧是我们在文本输入框的value =“”属性中使用了ASP.NET Literals,因此文本框本身不需要运行runat =“server”。然后将结果封装在一个ASP:Panel中,并在页面加载时设置Visible属性,以根据您是否想显示任何结果来显示结果。


它的功能相当不错,但URL可能不像StackOverflow那样友好。 - Mehrdad Afshari
1
URLs会非常友好,我认为这是一个非常好的解决方案。 - Jon Skeet
啊,我早先读了你的推文,已经做了研究,但现在我错过了你的问题,因为我要给我的小孩准备洗澡。 :-) - splattne

2

好的 Jon,首先讲解viewstate问题:

我没有检查过自2.0版本以来是否有任何内部代码更改,但这是我几年前处理viewstate的方法。实际上,该隐藏字段是在HtmlForm中硬编码的,因此您应该派生出您的新字段,并进入其渲染,自己进行调用。请注意,如果您坚持使用纯旧的输入控件(我猜您想这样做,因为它还可以帮助不需要客户端JS),您也可以将__eventtarget和__eventtarget排除在外:

protected override void RenderChildren(System.Web.UI.HtmlTextWriter writer)
{
    System.Web.UI.Page page = this.Page;
    if (page != null)
    {
        onFormRender.Invoke(page, null);
        writer.Write("<div><input type=\"hidden\" name=\"__eventtarget\" id=\"__eventtarget\" value=\"\" /><input type=\"hidden\" name=\"__eventargument\" id=\"__eventargument\" value=\"\" /></div>");
    }

    ICollection controls = (this.Controls as ICollection);
    renderChildrenInternal.Invoke(this, new object[] {writer, controls});

    if (page != null)
        onFormPostRender.Invoke(page, null);
}

所以你获取了这3个静态MethodInfo并调用它们,跳过了那个viewstate部分 ;)
static MethodInfo onFormRender;
static MethodInfo renderChildrenInternal;
static MethodInfo onFormPostRender;

以下是您表单的类型构造器:

static Form()
{
    Type aspNetPageType = typeof(System.Web.UI.Page);

    onFormRender = aspNetPageType.GetMethod("OnFormRender", BindingFlags.Instance | BindingFlags.NonPublic);
    renderChildrenInternal = typeof(System.Web.UI.Control).GetMethod("RenderChildrenInternal", BindingFlags.Instance | BindingFlags.NonPublic);
    onFormPostRender = aspNetPageType.GetMethod("OnFormPostRender", BindingFlags.Instance | BindingFlags.NonPublic);
}

如果我理解你的问题正确,你也不想在表单中使用POST作为操作方式,那么以下是实现方法:

protected override void RenderAttributes(System.Web.UI.HtmlTextWriter writer)
{
    writer.WriteAttribute("method", "get");
    base.Attributes.Remove("method");

    // the rest of it...
}

我想这就是全部了。让我知道进展如何。
编辑:我忘记了页面视图状态方法:
因此,您的自定义表单:HtmlForm 将获得全新的抽象(或非抽象)页面:System.Web.UI.Page :P
protected override sealed object SaveViewState()
{
    return null;
}

protected override sealed void SavePageStateToPersistenceMedium(object state)
{
}

protected override sealed void LoadViewState(object savedState)
{
}

protected override sealed object LoadPageStateFromPersistenceMedium()
{
    return null;
}

在这种情况下,我封装了方法,因为您无法封装页面(即使它不是抽象的,Scott Guthrie也会将其包装到另一个页面中:P),但您可以封装您的表单。

谢谢这个建议 - 尽管听起来需要投入很多工作。Dan的解决方案对我很好,但拥有更多选项总是不错的。 - Jon Skeet

1

你有没有考虑过不要消除POST,而是在表单被POST时重定向到适当的GET URL。也就是说,接受GET和POST,但在POST时构造一个GET请求并重定向到它。这可以通过页面或HttpModule处理,如果您想使其与页面无关。我认为这会让事情变得更容易。

编辑:我假设您已经在页面上设置了EnableViewState="false"。


好主意。强制去做的话可能很可怕,但从实际效果来看应该不错 :) 我会试一下... - Jon Skeet
是的,我已经尝试在各个地方使用EnableViewState=false。它并不能完全禁用它,只是减少了它的使用。 - Jon Skeet
Jon:如果您不使用该死的服务器控件(没有runat="server"),并且根本没有<form runat="server">,ViewState将不会成为问题。这就是为什么我说不要使用服务器控件。您可以始终使用Request.Form集合。 - Mehrdad Afshari
但是如果控件没有runat=server属性,当渲染时再次将值传递给控件会很麻烦。幸运的是,带有runat=server属性的HTML控件可以很好地解决这个问题。 - Jon Skeet

1
我会创建一个处理路由的HTTP模块(类似于MVC但不复杂,只是一些if语句),并将其传递给aspx或ashx页面。首选aspx,因为更容易修改页面模板。但我不会在aspx中使用WebControls,只用Response.Write。
顺便说一下,为了简化流程,您可以在模块中进行参数验证(因为它可能与路由共享代码),并将其保存到HttpContext.Items中,然后在页面中呈现它们。这基本上可以像MVC一样工作,没有所有的花哨功能。这是我在ASP.NET MVC时代之前经常做的事情。

1

我真的很高兴完全放弃Page类,只需使用基于url的大开关语句处理每个请求。每个“页面”都变成一个HTML模板和C#对象。模板类使用正则表达式与匹配委托进行比较,以便与键集合进行比较。

好处:

  1. 非常快,即使重新编译后几乎没有延迟(页面类必须很大)
  2. 控制非常细粒度(非常适合SEO,以及将DOM与JS良好地结合在一起)
  3. 演示与逻辑分离
  4. jQuery完全控制HTML

遗憾:

  1. 简单的东西需要花费更长的时间,在几个地方需要编写代码,但是它确实可以很好地扩展
  2. 总是会诱惑我只使用页面视图,直到我看到ViewState(呕吐),然后我就回到现实了。

Jon,我们周六早上在SO上做什么啊:)?


1
这里是周六晚上。这样可以吗?(顺便说一句,我很想看到我的发布时间/日期的散点图...) - Jon Skeet

1

我认为asp:Repeater控件已经过时了。

虽然ASP.NET模板引擎很好用,但你也可以用for循环轻松实现重复操作...

<form action="JonSkeetForm.aspx" method="get">
<div>
    <input type="text" ID="text1" runat="server" />
    <input type="text" ID="text2" runat="server" />
    <button type="submit">Submit</button>
    <% foreach( var item in dataSource ) { %>
        <div>Some text</div>   
    <% } %>
</div>
</form>

ASP.NET Forms还算可以,Visual Studio提供了不错的支持,但是那个runat="server"实在是错误的。ViewState也是。

我建议你看一下是什么让ASP.NET MVC如此出色,它摆脱了ASP.NET Forms的方法,而不是抛弃它。

你甚至可以编写自己的生成提供程序来编译自定义视图,比如NHaml。我认为你应该在这里寻找更多控制,并简单地依赖ASP.NET运行时来封装HTTP和作为CLR托管环境。如果你运行集成模式,那么你也可以操纵HTTP请求/响应。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接