Ileż to razy zmuszeni jesteśmy pisać kod temu podobny:
1: list.ValueMember = "Id"; 2: list.DisplayMember = "Name";
Na CodeGuru niejednokrotnie pytano o jakiś sposób na rozwiązanie tego problemu. Podawanie stringów jest ZŁE, niewygodne i bardzo podatne na błędy wszelakie. Zmiana nazwy właściwości rozwala UI, refactoring bez dodatkowych narzędzi jak Resharper potrafi napsuć sporo krwi (a i z pomocą R# wcale przyjemny nie jest)… Syf, kiła i mogiła.
Postanowiłem poeksperymentować w tym obszarze i oto co udało mi się osiągnąć. Mając klasę User:
1: public class User 2: { 3: public int Id { get; set; } 4: public string Name { get; set; } 5: }
Możemy zrobić coś takiego:
1: list.ValueMember<User>(u => u.Id); 2: list.DisplayMember<User>(u => u.Name);
Fajnie, prawda? Zobaczmy jak taka magia wygląda pod spodem.
Wykonanie tych instrukcji bez podawania nigdzie jawnie żadnego stringa możliwe jest dzięki LINQ, a dokładniej klasie Expression. O niej i wszystkim co się z nią wiąże napiszę być może kiedy indziej (a nawet jeśli nie to zachęcam do samodzielnego zapoznania się z tematem), dzisiaj natomiast ograniczę się do pokazania metod zademonstrowanych powyżej:
1: public static class ListControlExtensions 2: { 3: public static void ValueMember<T>(this ListControl _this, Expression<Func<T, object>> retrieve) 4: { 5: _this.ValueMember = GetMemberName(retrieve); 6: } 7: 8: public static void DisplayMember<T>(this ListControl _this, Expression<Func<T, object>> retrieve) 9: { 10: _this.DisplayMember = GetMemberName(retrieve); 11: } 12: 13: private static string GetMemberName<T>(Expression<Func<T, object>> retrieve) 14: { 15: Expression body = retrieve.Body; 16: 17: MemberExpression memberExpression; 18: 19: // reference type -> no conversion needed to object type 20: if (body is MemberExpression) 21: { 22: memberExpression = (MemberExpression)body; 23: } 24: // simple type => boxing/conversion needed to return object type 25: else if (body.NodeType == ExpressionType.Convert && ((UnaryExpression)body).Operand is MemberExpression) 26: { 27: memberExpression = (MemberExpression)((UnaryExpression)body).Operand; 28: } 29: else 30: { 31: throw new ArgumentException(string.Format("This lambda: {0} is not supported!", retrieve.Body.ToString())); 32: } 33: 34: return BuildMemberPath(memberExpression); 35: } 36: 37: private static string BuildMemberPath(MemberExpression memberExpression) 38: { 39: string thisName = memberExpression.Member.Name; 40: 41: if (memberExpression.Expression is MemberExpression) 42: return BuildMemberPath((MemberExpression)memberExpression.Expression) + "." + thisName; 43: return thisName; 44: } 45: }
Jak widać kluczowym elementem jest metoda GetMemberName. Zwraca ona podane wyrażenie lambda w postaci tekstowej, w sam raz do przypisania do DisplayMember (lub, jeśli już przy tym jesteśmy, do DataTextField w kontekście aplikacji web).
Takie operacje sprawdzane w czasie kompilacji są nam udostępnione, ponieważ jako parametr tych metod przyjmujemy Expression<Func<…>> a nie samo Func<…>. Dzięki temu cała instrukcja rozbita na części pierwsze stoi przed nami (p)otworem. Logika przedstawionych operacji jest bardzo prosta:
1) sprawdzamy, czy całe wyrażenie jest pobraniem wartości jakiejś składowej klasy –> jeśli tak, budujemy z niego tekst
2) jeśli nie, sprawdzamy, czy mamy przypadkiem do czynienia z rzutowaniem jako główną operacją wyrażenia; do mechanizmu możemy przekazać wyrażenie zwracające object (dokładniej: Func<T, object>), więc w przypadku typów prostych (jak np. int) musi zajść tzw. boxing; w tym przypadku zakładamy, że "podrzędne" do rzutowania wyrażenie jest pobraniem wartości ze składowej klasy i na nim opieramy budowanie tekstu
3) jeżeli żaden z powyższych scenariuszy nie jest spełniony, wyrzucamy wyjątek, na przykład dla wywołania
1: list.ValueMember<User>(u => u.Id + 666);
otrzymamy komunikat:
Budowanie tekstu to prosta rekurencja wędrująca po wywołaniach kolejnych składowych i sklejająca poszczególne części. Dzięki temu wywołanie:
1: list.ValueMember<User>(u => u.UserAddress.AddressCity.Name);
wygeneruje tekst: "UserAddress.AddressCity.Name".
ALE HOLA HOLA! Oczywiście takie wywołanie na niewiele się zda w przedstawianym scenariuszu. Jak wiadomo, zarówno Windows Forms ze swoimi ValueMember i DisplayMember jak i ASP.NET z DataValueField i DataTextField nie wspierają wywoływania zagnieżdżonych właściwości. Dlatego też pomimo faktu, że mamy mechanizm potrafiący spisać dowolnie skomplikowany łańcuch zagnieżdżeń, i tak wykorzystać możemy jedynie jego najprostsze działanie w postaci u => u.Id czy u => u.Age. Ale w końcu lepsze to niż to co mieliśmy wcześniej.
Tak czy siak zachęcam to eksplorowania tej działki .NETa, ponieważ można natknąć się na naprawdę interesujące zastosowania eliminujące dręczące nas problemy.
Jak każdy post – super
rewelacja, dzięki