W .net ASP.MVC są różne mechanizmy. Są też takie, które umożliwiają zbadanie argumentów przesłanych do akcji, jak i argumentów oczekiwanych w akcji. I właśnie o nich dzisiaj. Można je wykorzystywać na dobry i zły sposób, jak każde narzędzie. Najpierw mały pokaz a potem filozofowanie. Mamy taki kod:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: return this.View("Index");
6: }
7:
8: public string MustHaveString(string text)
9: {
10: return text;
11: }
12:
13: public string CanHaveString(string text = "")
14: {
15: return text;
16: }
17:
18: public string MustHaveInt(int input)
19: {
20: return input.ToString();
21: }
22:
23: public string CanHaveInt(int input = 0)
24: {
25: return input.ToString();
26: }
27:
28: public string MustHaveBoth(string text, int input)
29: {
30: string r = string.Empty;
31: for (int i = 0; i < input; i++)
32: {
33: r += text;
34: }
35: return r;
36: }
37:
38: public string CanHaveBoth(string text = "just a text", int input = 7)
39: {
40: string r = string.Empty;
41: for (int i = 0; i < input; i++)
42: {
43: r += text;
44: }
45: return r;
46: }
47:
48: public string Greed(string text, int input, int wantSoMuch, int stillNotUsing, int itAll)
49: {
50: return text + input;
51: }
52: }
Idąc od góry oczekujemy stringa, następnie oczekujemy stringa, ale mamy wartość domyślną na wypadek nie przesłania do przez użytkownika. Dalej to samo z intem, obowiązkowo oraz opcjonalnie. Potem wariacja z dwoma parametrami – obowiązkowo i opcjonalnie. I na koniec strasznie poznańska metoda, która potrzebuje wielu paramentrów, ale wykorzystuje tylko kilka z nich. Jeszcze jeszcze index, którego wyświetlenie wygląda tak:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: <div class="container">
2: <div class="row">
3: <div class="col-md-6">@Html.ActionLink("Must have a string with string set", "MustHaveString", "Home", new { text = "lorem ipsum" }, new { @class = "btn btn-default" })</div>
4: </div>
5: <div class="row">
6: <div class="col-md-6">@Html.ActionLink("Must have a string with string not", "MustHaveString", "Home", new {}, new { @class = "btn btn-default" })</div>
7: </div>
8: <div class="row">
9: <div class="col-md-6">@Html.ActionLink("Can have a string with string set", "CanHaveString", "Home", new { }, new { @class = "btn btn-default" })</div>
10: </div>
11: <div class="row">
12: <div class="col-md-6">@Html.ActionLink("Can have a string with string not set", "CanHaveString", "Home", new {text="litwo ojczyzno moja" }, new { @class = "btn btn-default" })</div>
13: </div>
14: <hr/>
15: <div class="row">
16: <div class="col-md-6">@Html.ActionLink("Must have a int with int set", "MustHaveInt", "Home", new { input = 42 }, new { @class = "btn btn-default" })</div>
17: </div>
18: <div class="row">
19: <div class="col-md-6">@Html.ActionLink("Must have a int with int not", "MustHaveInt", "Home", new { }, new { @class = "btn btn-default" })</div>
20: </div>
21: <div class="row">
22: <div class="col-md-6">@Html.ActionLink("Can have a int with int set", "CanHaveInt", "Home", new { input=667 }, new { @class = "btn btn-default" })</div>
23: </div>
24: <div class="row">
25: <div class="col-md-6">@Html.ActionLink("Can have a int with int not set", "CanHaveInt", "Home", new { }, new { @class = "btn btn-default" })</div>
26: </div>
27: <hr/>
28: <div class="row">
29: <div class="col-md-6">@Html.ActionLink("Must have both set", "MustHaveBoth", "Home", new { text="plonie ogniosko", input=3}, new { @class = "btn btn-default" })</div>
30: </div>
31: <div class="row">
32: <div class="col-md-6">@Html.ActionLink("Must have both not", "MustHaveBoth", "Home", new { }, new { @class = "btn btn-default" })</div>
33: </div>
34: <div class="row">
35: <div class="col-md-6">@Html.ActionLink("Must have both set too much", "MustHaveBoth", "Home", new { text="a",input="123", x="co ja tu"}, new { @class = "btn btn-default" })</div>
36: </div>
37: <hr/>
38: <div class="row">
39: <div class="col-md-6">@Html.ActionLink("Greed only the using ones", "Greed", "Home", new { text = "a", input = "123" }, new { @class = "btn btn-default" })</div>
40: </div>
41: <div class="row">
42: <div class="col-md-6">@Html.ActionLink("Greed all the things", "Greed", "Home", new { text = "a", input = "123", wantSoMuch = -1, stillNotUsing = -1, itAll =-1}, new { @class = "btn btn-default" })</div>
43: </div>
44: </div>
Czyli razor w linkami do poszczególnych akcji, czasem w linku są wymagane argumenty czasem ich brak. Wygląda to tak:
Mała niespodzianka, która wyszła podczas pisania tego posta. Okazuje się, że gdy na liście argumentów jest string, ale nie zostanie przesłany i nawet jeśli nie posiada on domyślnej wartości to i tak pewne mechanizmy asp zadziałają i zostanie wywołana akcja zdefiniowana w kontrolerze. Ta sama sztuczka z int’em już nie zadziała. Zostanie wyświetlmy zółty ekran błędu, informujący o tym, że nie udało się połączyć wszystkich kropek i jest klops. Ale to nie o tym wpis – ot taka ciekawostka.
W kontrolerze możemy nadpisać wywołanie metody, np. takiej metody w taki sposób:
1: protected override void OnActionExecuting(ActionExecutingContext filterContext)
2: {
3: Debug.WriteLine(
4: string.Format("***** New Action *****n***** {0} *****", filterContext.ActionDescriptor.ActionName));
5: Debug.WriteLine("*** Action parameteres {0}***", filterContext.ActionParameters.Count);
6: filterContext.ActionParameters.ForEach(
7: ap => Debug.WriteLine(string.Format("Key: {0}tValue: {1}", ap.Key, ap.Value)));
8:
9: var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
10: Debug.WriteLine("*** Query string {0}***", queryString.AllKeys.Count());
11: queryString.AllKeys.ForEach(k =>
12: Debug.WriteLine(string.Format("Key: {0}tValue: {1}", k, queryString[k])));
13: }
I teraz docieramy do sedna wpisu, będziemy oglądać QueryString i ActionParameters. Dla starych wyjadaczy różnica pewnie jasna i oczywista, ja się poznałem z nimi dopiero ostatnio, oryginalni autorzy świetnie się spisali przeinaczając (jest takie słowo) ich cel: otóż, wymyślił sobie ktoś że skoro w projekcie jest bardzo dużo kontrolerów, to pewnie znajdzie się część wspólna, którą warto umieścić w jakimś bazowym dla projektu kontrolerze. Sanity check – OK. Wszystko mogło być dobrze, do momenty gdy ktoś inny, nie robiłem git blame, nie wpadł na pomysł aby sprawdzać czy argumenty się zgadzają i sprawdzać, czy action paramentes zawiera np projectId, który ma zostać wysłany od użytkownika. Sanity check – NOK. Każdy kontroler dziedziczący ten wspólny, musi mieć na liście argumentów każdej ze swojej akcji parametr “int projectId”. Co przeciwnym wypadku? Oczywiście że przekierowanie na InvalidUrl i uwaga, strona ta w żaden sposób nie informuje o tym, czego oczekiwała, ani nic. Po prostu invalid url. Sanity check – Reject!
Dlaczego mnie to tak drażni? Bo jest to ukrywanie zależności danej metody, tworzę albo pracuje z taki kodem i gdy na liście argumentów mam nie używane parametry, to je usuwam i oczekuje że kod nadal będzie działać.
Zobaczcie jak wygląda wywołanie poszczególnych linków:
***** New Action *****
***** MustHaveString *****
*** Action parameteres 1***
Key: text Value: lorem ipsum
*** Query string 1***
Key: text Value: lorem ipsum
***** New Action *****
***** MustHaveString *****
*** Action parameteres 1***
Key: text Value:
*** Query string 0***
***** New Action *****
***** CanHaveString *****
*** Action parameteres 1***
Key: text Value:
*** Query string 0***
***** New Action *****
***** CanHaveString *****
*** Action parameteres 1***
Key: text Value: litwo ojczyzno moja
*** Query string 1***
Key: text Value: litwo ojczyzno moja
***** New Action *****
***** MustHaveInt *****
*** Action parameteres 1***
Key: input Value: 42
*** Query string 1***
Key: input Value: 42
***** New Action *****
***** MustHaveInt *****
*** Action parameteres 1***
Key: input Value:
*** Query string 0***
***** New Action *****
***** CanHaveInt *****
*** Action parameteres 1***
Key: input Value: 667
*** Query string 1***
Key: input Value: 667
***** New Action *****
***** CanHaveInt *****
*** Action parameteres 1***
Key: input Value: 0
*** Query string 0***
***** New Action *****
***** MustHaveBoth *****
*** Action parameteres 2***
Key: text Value: plonie ogniosko
Key: input Value: 3
*** Query string 2***
Key: text Value: plonie ogniosko
Key: input Value: 3
***** New Action *****
***** MustHaveBoth *****
*** Action parameteres 2***
Key: text Value:
Key: input Value:
*** Query string 0***
***** New Action *****
***** MustHaveBoth *****
*** Action parameteres 2***
Key: text Value: a
Key: input Value: 123
*** Query string 3***
Key: text Value: a
Key: input Value: 123
Key: x Value: co ja tu
***** New Action *****
***** Greed *****
*** Action parameteres 5***
Key: text Value: a
Key: input Value: 123
Key: wantSoMuch Value:
Key: stillNotUsing Value:
Key: itAll Value:
*** Query string 2***
Key: text Value: a
Key: input Value: 123
***** New Action *****
***** Greed *****
*** Action parameteres 5***
Key: text Value: a
Key: input Value: 123
Key: wantSoMuch Value: -1
Key: stillNotUsing Value: -1
Key: itAll Value: -1
*** Query string 5***
Key: text Value: a
Key: input Value: 123
Key: wantSoMuch Value: -1
Key: stillNotUsing Value: -1
Key: itAll Value: -1
O ile pierwsze z nich są w porządku, bo korzystają z tego czego wymagają, o tyle mały smród na końcu, niegodziwy zachłanny gostek chce aż pięciu parametrów, podczas gdy wykorzystuje tylko dwa z pięciu. Na szczęcie HomeController nie został jeszcze dotknięty ideą sprawdzania action parameters, ale jeśli ktoś wpadnie na pomysł napisania takiego kodu:
1: protected override void OnActionExecuting(ActionExecutingContext filterContext)
2: {
3: if (!filterContext.ActionParameters.Keys.Contains("itAll"))
4: {
5: Response.Redirect("http://jstadnicki.blogspot.com/");
6: }
7: }
To wtedy musicie dostarczyć mieć na liście argumentów metody, uwaga napiszę to jeszcze raz: sygnatura akcji musi mieć “itAll” na liście argumentów aby w ogóle się do niej dostać. Czyli wywołanie localhost/home/index?itAll=”dzialaj” nie zadziała, ponieważ index nie ma itAll na liście parametrów, a to że został on przesłany w query string nie pomoże.
Kończąc mój długi wpis, który miał być krótki. Nie ukrywajcie zależności w kodzie, zgłaszając błąd czy to użytkownikowi czy innemu programiście, podajcie wystarczająco dużo informacji, aby od razu jasne było dlaczego coś się nie udało. I dodając kod do klasy bazowej pamiętajcie o tym, jak wielki wpływ na inne części aplikacji będzie miała wasza zmiana. Dziedziczenie nie jest rozwiązaniem. I ostatnie jeszcze ActionParameters!!!!!!!!!!!!!!!=QueryString