W ramach nauki .NET Core piszę sobie aplikację webową. Na jednej podstronie mam wybór tygodnia, aby pokazywać dane z wybranego tygodnia.

Image

Użyłem do wyboru tygodnia standardowego znacznika HTML:

<input type="week">

Opis problemu

Ten znacznik przesyła dane w postaci 2020-W24, czyli rok, a następnie numer tygodnia. W C# nie ma żadnej klasy czy struktury temu odpowiadającej. Dlatego postanowiłem napisać sobie własną klasę Week.

Zawiera ona jeden konstruktor:

public Week(int year, int week)
{
    Year = year;
    WeekNr = week;
}

Przeciążyłem metodę ToString, aby zwracało informacje w formacie z kontrolki <input type="week">

public override string ToString()
{
    return $"{Year}-W{WeekNr}";
}

Zawiera jeszcze kilka metod pomocniczych:

public DateTime GetFirstDayOfWeek()
public static Week GetCurrentWeek()
public Week GetNextWeek()
public Week GetPreviousWeek()
public string GetDaysInWeekPeriod()

Wszystko spoko działa :-) Tylko za każdym razem jak przesyłam dane z formularza, to muszę stringa z informacją o tygodniu i roku przekształcać na obiekt Week. Chciałbym aby automatycznie to się robiło.

Ucząc się podstaw ASP.NET Core czytałem oczywiście o czymś taki jak bindowanie danych. Framework sam konwertuje teksty na odpowiedni typ, dzięki temu programista już nie musi się o to martwić. Skoro takie coś istnieje, to pomyślałem, że pewnie da się jakąś własną klasę napisać, która zamieni przychodzący tekst na obiekt Week.

Zacząłem googlować custom model binding i trafiłem na artykuł Custom Model Binding in ASP.NET Core z oficjalnej dokumentacji. Jest tam napisane jak zrobić własne bindowanie do własnego modelu.

Już zacząłem to robić. Dodałem klasę, która implementowała interfejs IModelBinder, ale na szczęście doczytałem artykuł do końca :-)

Disclaimer: Czasami zdarza mi się, że szybko chce coś zrobić i przeglądam tylko kod kopiuje go do siebie, a potem się denerwuje, że coś nie działa. Okazuje się, że coś pominę i nie doczytam… i nie działa.

Na końcu jest napisane:

Typically shouldn’t be used to convert a string into a custom type, a TypeConverter is usually a better option.

Użycie TypeConverter zamiast IModelBinder

I tak zrobiłem :-) I wszystko działa i wydaje się nawet, że jest to prostsze rozwiązanie. Co musiałem zrobić?

Dodać klasę WeekConverter, która dziedziczy po klasie TypeConverter:

public class WeekConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (sourceType == typeof(string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value is string isoWeek)
        {
            var result = isoWeek.Split("-W");
            return new Week(Convert.ToInt32(result[0]), Convert.ToInt32(result[1]));
        }

        return base.ConvertFrom(context, culture, value);
    }
}

Zaimplementowałem tylko 2 metody, których potrzebuję:

  • CanConvertFrom
  • ConvertFrom

TypeConverter konweruje w 2 strony ale mi tylko była potrzebna konwersja z typu string do Week.

Dodatkowo klasę Week musiałem oznaczyć atrybutem:

[TypeConverter(typeof(WeekConverter))]
public class Week

W metodzie oczywiście zmieniłem typ na Week + parę drobnych poprawek i już :-) I tylko tyle :-) Wszystko już działa.

Z formularza na stronie dostaję od razu obiekt Week, a nie string.

Podsumowanie

Jeśli możesz używaj TypeConverter zamiast własnego bindowania modelu. Czytaj dokumentację i artykuły do końca, mogą się tam znajdować ważne informacje.