views:

4344

answers:

7

I'm trying to build a very, very simple "micro-webapp" which I suspect will be of interest to a few Stack Overflow'rs if I ever get it done. I'm hosting it on my C# in Depth site, which is vanilla ASP.NET 3.5 (i.e. not MVC).

The flow is very simple:

  • If a user enters the app with a URL which doesn't specify all the parameters (or if any of them are invalid) I want to just display the user input controls. (There are only two.)
  • If a user enters the app with a URL which does have all the required parameters, I want to display the results and the input controls (so they can change the parameters)

Here are my self-imposed requirements (mixture of design and implementation):

  • I want the submission to use GET rather than POST, mostly so users can bookmark the page easily.
  • I don't want the URL to end up looking silly after submission, with extraneous bits and pieces on it. Just the main URL and the real parameters please.
  • Ideally I'd like to avoid requiring JavaScript at all. There's no good reason for it in this app.
  • I want to be able to access the controls during render time and set values etc. In particular, I want to be able to set the default values of the controls to the parameter values passed in, if ASP.NET can't do this automatically for me (within the other restrictions).
  • I'm happy to do all the parameter validation myself, and I don't need much in the way of server side events. It's really simple to set everything on page load instead of attaching events to buttons etc.

Most of this is okay, but I haven't found any way of completely removing the viewstate and keeping the rest of the useful functionality. Using the post from this blog post I've managed to avoid getting any actual value for the viewstate - but it still ends up as a parameter on the URL, which looks really ugly.

If I make it a plain HTML form instead of an ASP.NET form (i.e. take out runat="server") then I don't get any magic viewstate - but then I can't access the controls programmatically.

I could do all of this by ignoring most of ASP.NET and building up an XML document with LINQ to XML, and implementing IHttpHandler. That feels a bit low level though.

I realise that my problems could be solved by either relaxing my constraints (e.g. using POST and not caring about the surplus parameter) or by using ASP.NET MVC, but are my requirements really unreasonable?

Maybe ASP.NET just doesn't scale down to this sort of app? There's a very likely alternative though: I'm just being stupid, and there's a perfectly simple way of doing it that I just haven't found.

Any thoughts, anyone? (Cue comments of how the mighty are fallen, etc. That's fine - I hope I've never claimed to be an ASP.NET expert, as the truth is quite the opposite...)

+1  A: 

Have you thought about not eliminating the POST but rather redirecting to a suitable GET url when the form is POSTed. That is, accept both GET and POST, but on POST construct a GET request and redirect to it. This could be handled either on the page or via an HttpModule if you wanted to make it page-independent. I think this would make things much easier.

EDIT: I assume that you have EnableViewState="false" set on the page.

tvanfosson
Nice idea. Well, horrible idea in terms of being forced to do it, but nice in terms of it probably working :) Will try...
Jon Skeet
And yes, I've tried EnableViewState=false all over the place. It doesn't completely disable it, just cuts it down.
Jon Skeet
Jon: If you don't use the damned server controls (no runat="server") and you don't have a <form runat="server"> at all, ViewState will not be a trouble. That's why I said not to use server controls. You can always use Request.Form collection.
Mehrdad Afshari
But without runat=server on the controls, it's a pain to propagate the value to the controls again when rendering. Fortunately, HTML controls with runat=server work well.
Jon Skeet
+1  A: 

I would create an HTTP module that handles routing (similar to MVC but not sophisticated, just a couple if statements) and hand it to aspx or ashx pages. aspx is preferred since it's easier to modify the page template. I wouldn't use WebControls in the aspx however. Just Response.Write.

By the way, to simplify things, you can do parameter validation in the module (as it shares code with routing probably) and save it to HttpContext.Items and then render them in the page. This will work pretty much like the MVC without all the bell and whistles. This is what I did a lot before ASP.NET MVC days.

Mehrdad Afshari
+7  A: 

You're definitely (IMHO) on the right track by not using runat="server" in your FORM tag. This just means you'll need to extract values from the Request.QueryString directly, though, as in this example:

In the .aspx page itself:

<%@ 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"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<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>

and in the code-behind:

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));
        }
    }

The trick here is that we're using ASP.NET Literals inside the value="" attributes of the text inputs, so the text-boxes themselves don't have to runat="server". The results are then wrapped inside an ASP:Panel, and the Visible property set on page load depending whether you want to display any results or not.

Dylan Beattie
It works pretty well, but the URLs will not be as friendly as, say, StackOverflow.
Mehrdad Afshari
The URLs will be pretty friendly, I think... This looks like a really good solution.
Jon Skeet
Argh, I read your tweets earlier, had researched it, and now I missed your question preparing my ittle kids for the bathtub... :-)
splattne
+1  A: 

I've really been happy to totally abandon the page class altogether and just handler every request with a big switch case based on url. Evey "page" becomes a html template and a c# object. The template class uses a regex with a match delegate that compares against a key collection.

benefits:

  1. It's really fast, even after a recompile, there is almost no lag (the page class must be big)
  2. control is really granular (great for SEO, and crafting the DOM to play well with JS)
  3. the presentation is separate from logic
  4. jQuery has total control of the html

bummers:

  1. simple stuff takes a bit longer in that a single text box requires code in several places, but it does scale up really well
  2. it's always tempting to just do it with page view until i see a viewstate (urgh) then i snap back to reality.

Jon, what are we doing on SO on a Saturday morning:) ?

rizzle
It's Saturday evening here. Does that make it okay? (I'd love to see a scatter graph of my posting times/days, btw...)
Jon Skeet
+33  A: 

Here is a solution which is similar to Dylan's, with a few changes that (in my opinion) make the page simpler to manage. You have programmatic access to the controls in their entirety including all attributes on the controls. You'll also only end up putting the input text boxes in the URL on your GET request so your URL will be more "meaningful"

<%@ 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"&gt;

<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>

Then in your code-behind you can do everything you need on 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];
    }
}

If you don't want a form that has runat="server", then you should use HTML controls. It's easier to work with for your purposes. Just use regular HTML tags and put runat="server" and give them an ID. Then you can access them programmatically and code without a ViewState.

The only downside is that you won't have access to many of the "helpful" ASP.NET server controls like GridViews. I included a Repeater in my example because I'm assuming that you want to have the fields on the same page as the results and (to my knowledge) a Repeater is the only DataBound control that will run without a runat="server" attribute in the Form tag.

Dan Herbert
I've got so few fields that doing it manually is really easy :) The key was that I didn't know I could use runat=server with normal HTML controls. I haven't implemented the results yet, but that's the easy bit. Nearly there!
Jon Skeet
Indeed, a <form runat="server"> would add the __VIEWSTATE (and some other) hidden field even when you set EnableViewState="False" at page level. This is the way to go if you want to loose the ViewState on the page. As for Url friendliness, urlrewriting might be an option.
Sergiu Damian
No need for rewriting. This answer works fine (although it does mean having a control with an ID of "user" - for some reason I can't change the name of a textbox control separately from its ID).
Jon Skeet
Just to confirm, this worked very well indeed. Thanks very much!
Jon Skeet
Will this work if you are using master pages? The clientId of the controls usually change when using master pages.
Binoj Antony
This will work in Master Pages, you just have to use the ClientID property of your text boxes instead of hard-coding IDs into the Query String lookup.
Dan Herbert
Looks like you should have just written it in classic asp!
ScottE
+1  A: 

What the hell, I thought the asp:Repeater control was obsolete.

The ASP.NET template engine is nice but you can just as easily accomplish repeating with a for loop...

<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 is sort of okay, there's decent support from Visual Studio but this runat="server" thing, that's just wrong. ViewState to.

I suggest you take a look at what makes ASP.NET MVC so great, who it moves away from the ASP.NET Forms approach without throwing it all away.

You can even write your own build provider stuff to compile custom views like NHaml. I think you should look here for more control and simply relying on the ASP.NET runtime for wrapping HTTP and as a CLR hosting environment. If you run integrated mode then you'll be able to manipulate the HTTP request/response as well.

John Leidegren
+1  A: 

Okay Jon, the viewstate issue first:

I haven't checked if there's any kind of internal code change since 2.0 but here's how I handled getting rid of the viewstate a few years ago. Actually that hidden field is hardcoded inside HtmlForm so you should derive your new one and step into its rendering making the calls by yourself. Note that you can also leave __eventtarget and __eventtarget out if you stick to plain old input controls (which I guess you'd want to since it also helps not requiring JS on the client):

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);
}

So you get those 3 static MethodInfo's and call them out skipping that viewstate part out ;)

static MethodInfo onFormRender;
static MethodInfo renderChildrenInternal;
static MethodInfo onFormPostRender;

and here's your form's type constructor:

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);
}

If I'm getting your question right, you also want not to use POST as the action of your forms so here's how you'd do that:

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

    // the rest of it...
}

I guess this is pretty much it. Let me know how it goes.

EDIT: I forgot the Page viewstate methods:

So your custom Form : HtmlForm gets its brand new abstract (or not) Page : 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;
}

In this case I seal the methods 'cause you can't seal the Page (even if it isn't abstract Scott Guthrie will wrap it into yet another one :P) but you can seal your Form.

Thanks for this - although it sounds like rather a lot of work. Dan's solution worked fine for me, but it's always good to have more options.
Jon Skeet