MVC 3 jest fajne, nie jest idealne, ale jest bardzo dobre i można za pomocą jego stworzyć niesamowite rzeczy. Jednak mimo wszystko, MS zawsze potrafi pewne rzeczy utrudnić jak nie wiem co. W szczególności z jego ulubionym typem widoczności internal, który uważa chyba za święty nawet w aplikacjach open-source.

Jednym z takich ich faux pas moim zdaniem jest EditorFor, niby fajny, niby wystarczający, ale wystarczy, że chcemy dać inną klasę by wyróżnić dany element i już trzeba tworzyć EditorTemplate, a mnie to powinno … obchodzić. Czemu muszę się męczyć i tworzyć 10 szablonów w każdym z projektów? Rozumiem tworzenie szablonu, czasami trzeba, coś trzeba doprecyzować, obudować itp. itd. Jednak jest to drażniące i czasochłonne, zmarnowane minuty.

Weźmy dla przykładu typ int. Nadaje mu w ViewModel parametry walidacyjne: wymagany, oraz przedział (min, max). Wykorzystuje EditorFor i co otrzymuje? Pole tekstowe, w które mogę wprowadzić wszystko jak mi się żywnie podoba całkowicie olewające moje oznaczenia min, max – przecież biedny użytkownik, ma napisane podaj liczbę a może wpisać „mam was…” i działa do póki nie będzie próbował tego zapisać – oczywiście jeżeli następuje sprawdzenie poprawności danych.

Co teraz my musimy zrobić by umożliwić użytkownikowi wprowadzanie tylko liczb? Po wygenerowanych atrybutach nie mamy możliwości zorientowania się które pole może akceptować tylko liczby. Więc tworzymy własny EditorTemplate dla Int32, nadajemy mu jakąś znaczącą klasę i następnie już z wykorzystaniem jQuery odpowiednio blokujemy dostęp do zbędnych znaków, zezwalając tylko na cyfry.

Ale teraz mamy sytuację, że na stronie musimy domyślnie wyróżnić 2 z pięciu pól przyjmujących int, a więc albo tworzymy osoby EditorTemplate albo zabawiamy się w ViewData i przekazujemy w wywołaniu EditorFor odpowiednie dane, które następnie interpretujemy w naszym EditorTemplate, który zaczyna przypominać spaghetti (if coś, to coś, w przeciwnym wypadku … i tak dalej).

Więc wpadamy na pomysł by stworzyć własne rozszerzenie Html.Xxx które nam ten problem załatwi, ale teraz musimy wszędzie wykorzystać Xxx tam gdzie chcemy mieć int lub tam gdzie chcemy mieć specjalny przypadek int.

A nie prościej by było gdybyśmy nie musieli w ogóle o tym myśleć? Dla mnie tak. Dlatego stworzyłem rozszerzenieHtml5EditorFor, które bez żadnych zmian (wystarczy podmienić EditorFor na Html5EditorFor) będzie działać tak samo jak EditorFor. Jednak ma on kilka dodatkowych bajerów, które ułatwiają życie.

Zaczęło się prosto, potrzebowałem czegoś co umożliwi przekazanie mi atrybutów – dla pewnych pól potrzebowałem znaczników, mogłem wykorzystać TextBoxFor, ale nie chciałem, nie chcę za każdym razem myśleć w View czy to jest bool, czy to jest string a może to jest obiekt i potrzebuje załadować szablon dla niego. Chciałem móc tylko dodać atrybuty jeżeli jest to wymagane, ale nie psując przy tym funkcjonalności EditorFor.

Dla przykładu, gdybyśmy chcieli stworzyć element input który zawierałby atrybut data-placeholder wystarczy napisać:

Html.Html5EditorFor(model => model.MyInt, null, attributes: new { data_placeholder = "my-placeholder"})

Niestety, nazwa parametru attributes jest wymagana – inaczej .NET wybierze inną metodę. Jest to wina nie chcenia niszczenia aktualnej funkcjonalności EditorFor, chodziło o to by konwersja była tak prosta jak to tylko możliwe copy-replace.

Ale pisząc to rozszerzenie zauważyłem, że mam możliwość wprowadzenia pewnych dodatkowych rzeczy – jak dodatkowe klasy CSS.

Pierwszą rzeczą, którą zrobiłem to usunąłem EditorTemplates dla wszystkich liczbowych wartości i zastąpiłem je rozszerzonymi wbudowanymi szablonami, czyli jeżeli wywołujemy Html5EditorFor na typie int, to automatycznie nasze pole otrzymuje dodatkową klasę CSS numeric-int, zaś decimal numeric-float. Dzięki czemu można łatwo odnaleźć wymagane pola i na przykład za pomocą pluginu numeric dla jQuery przekształcić je tylko w pola numeryczne.

I od tego się to wszystko zaczęło.

Aktualna postać Html5EditorFor, umożliwia:

  • Czytanie atrybutów z poziomu ViewModel i odpowiednie generowanie atrybutów lub ich wartości w HTMLu – dla przykładu data-ui-tooltip, może być określony na własności w ViewModel i zostanie on wyrenderowany na stronie HTML. Tutaj nie ma ograniczenia do data-*, to równie dobrze może być atrybut required czy też klasa z CSS, która ma zostać dodana do domyślnych klas;
  • Dodanie odpowiednich klas CSS dla pól typu liczbowego;
  • Ustawienie HTML 5 input type na podstawie typu własności, lub za pomocą DataTypeAttribute czy też dodatkowych atrybutów dostępnych wraz z rozszerzeniem jak Html5TimeAttribute;
  • Czytanie i rozumienie atrybuty Range z .NET, dzięki któremu automatycznie ustawiane są atrybuty Min i Max w elemencie input – jest też dostępny atrybut Html5Range.

Dla przykładu następujący model:

public class NumericViewModel
{
    public byte Byte { get; set; } 
    public short Short { get; set; }
        
    [Html5Step(5)]
    [Html5UseDefaultMinMax]
    public int Int { get; set; }

    [Html5Range]
    [Html5UseDefaultMinMax]
    public long Long { get; set; } 
        
    [Range(10, 100)]
    public float Float { get; set; } 
        
    public double Double { get; set; } 
        
    [Html5Step(0.5)]
    public decimal Decimal { get; set; } 
}

Zostanie wyświetlony w ten sposób (dodatkowy atrybut w Html5EditorFor nadany dla Byte):

numeric-opera

W porównaniu z EditorFor:

numeric-std

Gdzie można znaleźć Html5EditorFor ?

Pełny zestaw przykładów jest dostępnych na github wraz z kodem źródłowym rozszerzenia. Zaś biblioteka jest do pobrania z nuget lub Install-Package Html5EditorFor.

Istniejące atrybuty

  • Html5InputTypeAttribute – umożliwia ustawienie typ input na dowolny typ z dostępnych w HTML 5 plus Text i Tel.
  • Html5NumberAttribute – ustawia typ input na number, równoważne z ustawieniem DataType.Currency;
  • Html5RangeAttribute – ustawia typ input na range, umożliwia podanie wartości min i max;
  • Html5StepAttribute – dodaje atrybut step;
  • Html5ColorAttribute – ustawia typ input na color;
  • Html5TimeAttribute – ustawia typ input na time, równoważne z ustawieniem DataType.Time;
  • Html5WeekAttribute – ustawia typ input na week;
  • Html5MonthAttribute – ustawia typ input na month;
  • Html5DateAttribute – ustawia typ input na date, równoważne z ustawieniem DataType.Date;
  • Html5DateTimeAttribute – ustawia typ input na datetime (czas UTC);
  • Html5DateTimeLocalAttribute – ustawia typ input na datetime-local (czas lokalny), równoważne z ustawieniem DataType.DateTime;
  • Html5SearchAttribute – ustawia typ input na search;
  • Html5EmailAttribute – ustawia typ input na email, równoważne z ustawieniem DataType.EmailAddress;
  • Html5PasswordAttribute – ustawia typ input na password, równoważne z ustawieniem DataType.Password;
  • Html5UrlAttribute – ustawia typ input na url, równoważne z ustawieniem DataType.ImageUrl lub DataType.Url;
  • Html5PhoneAttribute– ustawia typ input na password, równoważne z ustawieniem DataType.PhoneNumber;
  • Html5MaxAttribute – ustawia atrybut max;
  • Html5MinAttribute – ustawia atrybut min;
  • Html5UseDefaultMinMaxAttribute – ustawia atrybuty min i max bazując na type (na przykład byte.Min/MaxValue);
  • Html5CustomDataAttribute – umożliwia dodawanie dowolnego atrybuty data-* do wygenerowanego elementu input;
  • Html5DataAttribute – klasa bazowa dla atrybutów data-*;
  • HtmlAttribute – klasa bazowa dla każdego innego atrybutu.

Uwaga: dla pól typu liczbowego nie trzeba ustawiać atrybuty numer. Jest on domyślnie ustawiany.

Rozszerzalność

Istnieje możliwość tworzenia własnych atrybutów, które będą rozumiane przez Html5EditorFor, wystarczy tylko przeciążyć atrybut HtmlAttribute dla dowolnego atrybutu, lub Html5DataAttribute dla atrybutów zaczynających się od data-*. Dla przykładu, gdybyśmy chcieli generować z ViewModel atrybut data-placeholder możemy to zrobić na dwa sposoby, (1) wykorzystać już utworzony Html5CustomDataAttribute i podać wartości placeholder i wartość, lub (2):

public class PlaceholderAttribute : Html5DataAttribute 
{
    public PlaceholderAttribute(string value) : base("placeholder", value) {}
}

// usage

[Placeholder("address")]
public string MyProp { get; set; }

Wady

Ze względu na generowanie inputy dla HTML 5 Opera i Chrome same wyświetlają własne różne wiadomości walidacyjne (różny sposób wyświetlania), co jest trochę drażniące – domyślne błędy walidacyjne też są wyświetlane.

Domyślne ustawianie wartości number, ale to było potrzebne do mojego projektu, można to zmienić.

Także mimo, że fajnie jest mieć Html5EditoFor, to czasami można o nim zapomnieć (wygenerowany jest EditorFor) :(

Feedback

Szczerze mówiąc, jedynym testem tego rozszerzenia to były dwa moje projekty, więc sam jestem ciekaw jak to się zachowa w innych projektach. Jeżeli znajdziecie jakiś bug to dajcie znać najlepiej na github.

Także dajcie znać co o tym myślicie i czy w ogóle warto waszym zdaniem takie rozszerzenie wykorzystywać/rozwijać.

Linki

  • NuGet Package – dostępny jest tutaj;
  • GitHub – kod źródłowy oraz projekt wykorzystujący rozszerzenie dostępny jest tutaj.