Jak mogę uzyskać większą kontrolę w ASP.NET?

124

Próbuję zbudować bardzo, bardzo prostą „mikro-aplikację internetową”, która, jak podejrzewam, zainteresuje kilku użytkowników Stack Overflow, jeśli kiedykolwiek to zrobię. Hostuję go w mojej witrynie C # in Depth, która jest waniliowym ASP.NET 3.5 (tj. Nie MVC).

Przepływ jest bardzo prosty:

  • Jeśli użytkownik wejdzie do aplikacji z adresem URL, który nie określa wszystkich parametrów (lub jeśli którykolwiek z nich jest nieprawidłowy), chcę po prostu wyświetlić kontrolki wprowadzania danych przez użytkownika. (Są tylko dwa.)
  • Jeżeli użytkownik wprowadzi aplikację z adresem URL, który robi wszystkie wymagane parametry, chcę wyświetlić wyniki i formantów wejściowych (dzięki czemu mogą one zmieniać parametry)

Oto moje narzucone przeze mnie wymagania (połączenie projektu i realizacji):

  • Chcę, aby zgłoszenie korzystało z funkcji GET, a nie POST, głównie po to, aby użytkownicy mogli łatwo dodać stronę do zakładek.
  • I nie chcą URL skończyć patrząc głupie po złożeniu, z obcych kawałki na nim. Podaj tylko główny adres URL i rzeczywiste parametry.
  • Najlepiej byłoby, gdyby w ogóle nie wymagał JavaScript. W tej aplikacji nie ma ku temu żadnego powodu.
  • Chcę mieć dostęp do formantów w czasie renderowania i ustawiania wartości itp. W szczególności chcę mieć możliwość ustawienia domyślnych wartości formantów na wartości parametrów przekazane, jeśli ASP.NET nie może tego zrobić automatycznie dla mnie (w ramach innych ograniczeń).
  • Cieszę się, że sam wykonuję walidację wszystkich parametrów i nie potrzebuję wiele w zakresie zdarzeń po stronie serwera. Naprawdę łatwo jest ustawić wszystko podczas ładowania strony zamiast dołączać zdarzenia do przycisków itp.

Większość z nich jest w porządku, ale nie znalazłem żadnego sposobu na całkowite usunięcie stanu widoku i zachowanie pozostałych przydatnych funkcji. Korzystając z posta z tego posta na blogu , udało mi się uniknąć uzyskania jakiejkolwiek rzeczywistej wartości stanu widoku - ale nadal kończy się to jako parametr adresu URL, który wygląda naprawdę brzydko.

Jeśli zrobię to jako zwykły formularz HTML zamiast formularza ASP.NET (tj. Wyjmij runat="server"), nie otrzymam żadnego magicznego stanu widoku - ale nie mogę uzyskać programowego dostępu do elementów sterujących.

I mógłby zrobić to wszystko, pomijając większość ASP.NET i budowania dokumentu XML z LINQ do XML i wykonawczych IHttpHandler. To jednak wydaje się nieco niski poziom.

Zdaję sobie sprawę, że moje problemy można rozwiązać albo przez złagodzenie moich ograniczeń (np. Używając POST i nie dbając o nadwyżkowy parametr) lub używając ASP.NET MVC, ale czy moje wymagania są naprawdę nieracjonalne?

Może po prostu nie ASP.NET skalowanie w dół do tego typu aplikacji? Jest jednak bardzo prawdopodobna alternatywa: po prostu jestem głupi i jest na to całkiem prosty sposób, którego po prostu nie znalazłem.

Jakieś myśli, ktoś? (Komentarze wskazujące, jak upadli potężni itp. W porządku - mam nadzieję, że nigdy nie twierdziłem, że jestem ekspertem ASP.NET, ponieważ prawda jest zupełnie inna ...)

Jon Skeet
źródło
16
„Komentarze wskazujące na to, jak upadli potężni” - wszyscy jesteśmy ignorantami, tylko w różnych sprawach. Dopiero niedawno zacząłem tu uczestniczyć, ale bardziej podziwiam to pytanie niż wszystkie punkty. Oczywiście nadal myślisz i się uczysz. Uznanie dla ciebie.
duffymo
15
Nie sądzę, żebym kiedykolwiek zwrócił uwagę na kogoś, kto zrezygnował z nauki :)
Jon Skeet
1
Prawda w ogólnym przypadku. Bardzo prawdziwe w informatyce.
Mehrdad Afshari
3
Czy następną książką będzie „ASP.NET in Depth”? :-P
chakrit
20
Tak, ma się ukazać w 2025 roku;)
Jon Skeet

Odpowiedzi:

76

To rozwiązanie zapewnia programowy dostęp do całości kontrolek, w tym wszystkich atrybutów kontrolek. Ponadto po przesłaniu w adresie URL pojawią się tylko wartości pola tekstowego, dzięki czemu adres URL żądania GET będzie bardziej „znaczący”

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

Następnie w swoim kodzie możesz zrobić wszystko, czego potrzebujesz w 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];
    }
}

Jeśli nie chcesz mieć formularza, który ma runat="server", powinieneś użyć kontrolek HTML. Łatwiej jest pracować z Twoimi celami. Po prostu użyj zwykłych tagów HTML oraz umieść runat="server"i nadaj im identyfikator. Następnie możesz uzyskać do nich dostęp programowo i kodować bez rozszerzenia ViewState.

Jedynym minusem jest to, że nie będziesz mieć dostępu do wielu „pomocnych” kontrolek serwera ASP.NET, takich jak GridViews. RepeaterW moim przykładzie umieściłem a, ponieważ zakładam, że chcesz, aby pola znajdowały się na tej samej stronie co wyniki i (o ile mi wiadomo) a Repeaterjest jedyną kontrolką DataBound, która będzie działać bez runat="server"atrybutu w tagu Form.

Dan Herbert
źródło
1
Mam tak mało pól, że zrobienie tego ręcznie jest naprawdę proste :) Kluczem było to, że nie wiedziałem, że mogę używać runat = server z normalnymi kontrolkami HTML. Nie wdrożyłem jeszcze wyników, ale to łatwa część. Prawie tu!
Jon Skeet
Rzeczywiście, <form runat = "server"> dodałby pole __VIEWSTATE (i inne) ukryte nawet po ustawieniu EnableViewState = "False" na poziomie strony. To jest droga, jeśli chcesz stracić ViewState na stronie. Jeśli chodzi o przyjazność dla adresów URL, opcją może być zapisywanie adresów URL.
Sergiu Damian
1
Nie ma potrzeby przepisywania. Ta odpowiedź działa dobrze (chociaż oznacza to posiadanie kontrolki z identyfikatorem „użytkownika” - z jakiegoś powodu nie mogę zmienić nazwy kontrolki pola tekstowego niezależnie od jej identyfikatora).
Jon Skeet
1
Aby potwierdzić, to rzeczywiście działało bardzo dobrze. Dziękuję bardzo!
Jon Skeet
14
Wygląda na to, że powinieneś był napisać to w klasycznym asp
ScottE
12

Zdecydowanie (IMHO) jesteś na dobrej drodze, nie używając runat = "server" w swoim tagu FORM. Oznacza to po prostu, że musisz wyodrębnić wartości bezpośrednio z obiektu Request.QueryString, jak w tym przykładzie:

W samej stronie .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>

a w kodzie:

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

Sztuczka polega na tym, że używamy literałów ASP.NET wewnątrz atrybutów value = "" danych wejściowych tekstu, więc same pola tekstowe nie muszą uruchamiaćat = "server". Wyniki są następnie pakowane w ASP: Panel, a właściwość Visible jest ustawiana podczas ładowania strony, w zależności od tego, czy chcesz wyświetlić wyniki, czy nie.

Dylan Beattie
źródło
Działa całkiem dobrze, ale adresy URL nie będą tak przyjazne jak, powiedzmy, StackOverflow.
Mehrdad Afshari
1
Myślę, że adresy URL będą całkiem przyjazne ... Wygląda na to, że to naprawdę dobre rozwiązanie.
Jon Skeet
Argh, czytałem twoje tweety wcześniej, badałem je i teraz przegapiłem twoje pytanie przygotowując moje małe dzieci do wanny ... :-)
splattne
2

Dobra Jon, najpierw problem z wyświetlaniem:

Nie sprawdzałem, czy od wersji 2.0 nastąpiła jakaś wewnętrzna zmiana kodu, ale oto jak poradziłem sobie z pozbyciem się stanu widoku kilka lat temu. Właściwie to ukryte pole jest zakodowane na stałe w HtmlForm, więc powinieneś wyprowadzić nowy i przejść do jego renderowania, wykonując wywołania samodzielnie. Zwróć uwagę, że możesz również pozostawić __eventtarget i __eventtarget poza, jeśli trzymasz się zwykłych starych kontrolek wejściowych (które, jak sądzę, chciałbyś, ponieważ pomaga to również nie wymagać JS na kliencie):

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

Więc dostajesz te 3 statyczne MethodInfo i wywołujesz je, pomijając tę ​​część stanu widoku;)

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

a oto konstruktor typu twojego formularza:

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

Jeśli dobrze rozumiem twoje pytanie, nie chcesz również używać POST jako akcji twoich formularzy, więc oto jak to zrobić:

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

    // the rest of it...
}

Myślę, że to prawie wszystko. Daj mi znać jak idzie.

EDYCJA: Zapomniałem metod wyświetlania stanu strony:

Tak więc Twój niestandardowy Form: HtmlForm otrzymuje zupełnie nową abstrakcję (lub nie) Strona: 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;
}

W tym przypadku pieczętuję metody, ponieważ nie możesz zapieczętować Strony (nawet jeśli nie jest abstrakcyjna, Scott Guthrie zawinie ją w jeszcze jedną: P), ale możesz zapieczętować swoją Formę.

user134706
źródło
Dzięki za to - choć brzmi to raczej na dużo pracy. Rozwiązanie Dana działało dobrze dla mnie, ale zawsze dobrze jest mieć więcej opcji.
Jon Skeet
1

Czy myślałeś o nie eliminowaniu POST, ale raczej o przekierowaniu do odpowiedniego adresu URL GET, gdy formularz jest POST. Oznacza to, że zaakceptuj zarówno GET, jak i POST, ale w POST skonstruuj żądanie GET i przekieruj do niego. Można to zrobić na stronie lub za pośrednictwem modułu HttpModule, jeśli chcesz, aby był niezależny od strony. Myślę, że to znacznie ułatwiłoby sprawę.

EDYCJA: Zakładam, że na stronie ustawiono EnableViewState = "false".

tvanfosson
źródło
Dobry pomysł. Cóż, okropny pomysł pod względem zmuszony to zrobić, ale ładny pod względem Prawdopodobnie roboczych :) Będzie spróbować ...
Jon Skeet
I tak, wszędzie wypróbowałem EnableViewState = false. Nie wyłącza go całkowicie, po prostu go odcina.
Jon Skeet
Jon: Jeśli nie używasz przeklętych kontrolek serwera (no runat = "server") i nie masz w ogóle <form runat = "server">, ViewState nie będzie problemem. Dlatego powiedziałem, żeby nie używać kontrolek serwera. Zawsze możesz skorzystać z kolekcji Request.Form.
Mehrdad Afshari
Ale bez runat = server na kontrolkach ponowne propagowanie wartości do kontrolek podczas renderowania jest trudne. Na szczęście kontrolki HTML z runat = server działają dobrze.
Jon Skeet
1

Stworzyłbym moduł HTTP, który obsługuje routing (podobny do MVC, ale nie wyrafinowany, tylko kilka ifinstrukcji) i przekazałbym go aspxlub ashxstronom. aspxjest preferowany, ponieważ łatwiej jest modyfikować szablon strony. Nie użyłbym WebControlsw tym aspxjednak. Po prostu Response.Write.

Nawiasem mówiąc, aby uprościć sprawę, możesz przeprowadzić walidację parametrów w module (ponieważ prawdopodobnie współdzieli kod z routingiem) i zapisać go, HttpContext.Itemsa następnie wyrenderować na stronie. To będzie działać podobnie jak MVC bez wszystkich dzwonków i gwizdków. To właśnie robiłem przed dniami ASP.NET MVC.

Mehrdad Afshari
źródło
1

Naprawdę się cieszę, że całkowicie porzuciłem klasę strony i po prostu obsługuję każde żądanie za pomocą dużego przełącznika opartego na adresie URL. Każda „strona” staje się szablonem HTML i obiektem ac #. Klasa szablonu używa wyrażenia regularnego z delegatem dopasowania, który porównuje się z kolekcją kluczy.

korzyści:

  1. Jest naprawdę szybki, nawet po ponownej kompilacji, prawie nie ma opóźnienia (klasa strony musi być duża)
  2. kontrola jest bardzo szczegółowa (świetna do SEO i tworzenia DOM, aby dobrze współpracował z JS)
  3. prezentacja jest oddzielona od logiki
  4. jQuery ma całkowitą kontrolę nad html

bummers:

  1. proste rzeczy trwają trochę dłużej, ponieważ pojedyncze pole tekstowe wymaga kodu w kilku miejscach, ale skaluje się naprawdę dobrze
  2. zawsze kusi, aby po prostu zrobić to z odsłoną strony, dopóki nie zobaczę stanu wyświetlenia (ugh), a potem wracam do rzeczywistości.

Jon, co robimy w SO w sobotę rano :)?

missaghi
źródło
1
Tu jest sobotni wieczór. Czy to sprawia, że ​​jest w porządku? (Chciałbym zobaczyć wykres punktowy przedstawiający moje czasy / dni publikacji, przy okazji ...)
Jon Skeet,
1

Myślałem, że asp: sterowanie repeaterem było przestarzałe.

Silnik szablonów ASP.NET jest fajny, ale równie łatwo można powtórzyć za pomocą pętli 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 jest w porządku, program Visual Studio zapewnia przyzwoitą obsługę, ale ta funkcja runat = "server" jest po prostu błędna. ViewState to.

Proponuję przyjrzeć się temu, co sprawia, że ​​ASP.NET MVC jest tak świetny, czyli kto odchodzi od podejścia ASP.NET Forms bez odrzucania tego wszystkiego.

Możesz nawet napisać własne materiały dotyczące dostawcy kompilacji, aby skompilować niestandardowe widoki, takie jak NHaml. Myślę, że powinieneś szukać tutaj większej kontroli i po prostu polegać na środowisku wykonawczym ASP.NET do pakowania HTTP i jako środowiska hostingowego CLR. Jeśli uruchomisz tryb zintegrowany, będziesz mógł również manipulować żądaniem / odpowiedzią HTTP.

John Leidegren
źródło